Django 1.1 测试和调试教程(四)
原文:
zh.annas-archive.org/md5/ECB5EEA8F49C43CEEB591D269760F77D译者:飞龙
第十章:当一切都失败时:寻求外部帮助
有时我们遇到的问题似乎不是由我们自己的代码引起的。尽管我们按照最好的理解遵循文档,但我们得到的结果与我们的预期不符。构建在 Django 等开源代码上的好处之一是我们可以深入研究其代码,确切地找出问题出在哪里。然而,这可能不是我们时间的最佳利用方式。
追踪此类问题的更好的第一步通常是查阅社区资源。也许其他人已经遇到了我们面临的问题,并找到了解决方法或解决方法。如果是这样,我们可能可以通过利用他们的经验来节省大量时间,而不是找到自己的解决方案。
本章描述了 Django 社区资源,并说明了如何使用它们。具体来说,在本章中我们将:
-
通过发现 Django 1.1 版本中存在的一个错误的过程,并且导致了一些调查应用代码的问题
-
看看 Django 网站上提供的资源如何用于研究问题
-
根据研究结果讨论解决问题的最佳方法,无论是针对特定问题还是一般问题
-
了解其他获取帮助的途径,以及如何最好地利用它们
在 Django 中追踪问题
本书是使用写作时最新的 Django 版本编写的。一开始是 Django 1.1。然后,在写作过程中,发布了 Django 1.1.1,之后的所有内容都使用了 Django 1.1.1。该版本号中的三个 1 是主要、次要和微小的发布号。(缺少微小号,如 Django 1.1,意味着是 0。)Django 1.1.1 由于有明确的微小号,被称为微小发布。微小发布中唯一的更改是错误修复,因此微小发布与上一个版本完全兼容。虽然主要或次要版本号的更改可能涉及一些需要代码调整的不兼容更改,但更新到新的微小发布的唯一区别是减少了错误。因此,始终建议运行您正在使用的主要.次要版本的最新微小发布。
尽管有这样的建议和兼容性保证,有时候不升级到最新的可用版本是很诱人的。升级需要一些(可能很小,但不为零)工作量。此外,还有一个常识公理:如果它没有坏,就不要修理它。如果您实际上没有遇到任何问题,为什么要升级呢?
当 Django 1.1.1 发布时,我正好有这些想法。该发布恰好发生在写作中间第七章,“当车轮脱落时:理解 Django 调试页面”,这是一个充满了包含 Django 代码的跟踪的截图和控制台显示的章节。如果我在写作该章节的中间改变了 Django 代码库,即使只是微小的发布,谁知道早期和晚期章节的跟踪之间可能会引入什么微妙的差异?这样的差异可能会让敏锐的读者感到困惑。
如果我在中间升级,最安全的做法是重新做所有的示例,以确保它们是一致的。这是一个不太吸引人的选择,因为这既需要相当多的工作,又容易出错。因此,当 Django 1.1.1 发布时,我的最初倾向是延迟升级,至少直到下一章节的休息时间。
然而,最终我发现我确实不得不在章节中间升级,因为我遇到了一个 Django 错误,这个错误被 1.1.1 版本修复了。接下来的几节描述了遇到的错误,并展示了如何将其追踪到在 Django 1.1.1 中已经修复的问题。
重新审视第七章投票表单
回想一下,在第七章中,我们实现了显示活动调查的代码。这包括一个表单,允许用户为调查中的每个问题选择答案。对表单代码所做的最终更改之一涉及自定义错误格式。QuestionVoteForm的最终代码如下:
class QuestionVoteForm(forms.Form):
answer = forms.ModelChoiceField(widget=forms.RadioSelect,
queryset=None,
empty_label=None,
error_messages={'required': 'Please select an answer below:'})
def __init__(self, question, *args, **kwargs):
super(QuestionVoteForm, self).__init__(*args, **kwargs)
self.fields['answer'].queryset = question.answer_set.all()
self.fields['answer'].label = question.question
self.error_class = PlainErrorList
from django.forms.util import ErrorList
class PlainErrorList(ErrorList):
def __unicode__(self):
return u'%s' % ' '.join([e for e in self])
在__init__期间包含PlainErrorList类,并将表单实例的error_class属性设置为它,旨在将问题的错误显示从 HTML 无序列表(默认行为)更改为简单字符串。然而,当在 Django 1.1 下运行此代码,并尝试提交两个问题都未回答的调查以强制出现错误时,显示的结果是:
在两个错误消息左侧添加了项目符号表明错误列表仍然被格式化为 HTML 无序列表。这也可以通过检查页面的 HTML 源代码来确认,其中包括每个错误消息的以下片段:
<ul class="errorlist"><li>Please select an answer below:</li></ul>
似乎设置error_class属性没有任何效果。我们如何最好地追踪这样的问题?
实际上是否运行了正确的代码?
首先,我们需要确保正在运行的代码实际上是我们认为正在运行的代码。在这种情况下,当我遇到问题时,我可以看到开发服务器在添加PlainErrorList类和设置error_class属性的代码更改后重新启动,因此我非常确定正在运行正确的代码。尽管如此,在error_class赋值之前插入import pdb; pdb.set_trace()允许我确认代码已经存在并且正在按照我期望的方式运行:
> /dj_projects/marketr/survey/forms.py(14)__init__()
-> self.error_class = PlainErrorList
(Pdb) self.error_class
<class 'django.forms.util.ErrorList'>
(Pdb) s
--Return--
> /dj_projects/marketr/survey/forms.py(14)__init__()->None
-> self.error_class = PlainErrorList
(Pdb) self.error_class
<class 'survey.forms.PlainErrorList'>
(Pdb) c
在进入调试器之前,我们可以看到error_class的值为django.forms.util.ErrorList。通过赋值的步骤显示__init__方法即将返回,并再次检查error_class属性的值显示确实已将值更改为我们自定义的PlainErrorList。这一切看起来都很好。在表单创建代码的最后,error_class属性已设置为自定义类。为什么它没有被使用?
代码是否符合文档要求?
下一步(在移除添加的断点后)是再次检查文档。尽管似乎不太可能,也许还需要其他内容才能使用自定义错误类?重新检查文档后,似乎并没有。有关自定义错误类的完整文档如下:
提供的示例所做的事情与QuestionVoteForm所做的事情有一些细微差异。首先,提供的示例在表单创建时将错误类作为参数传递,因此它被传递给了表单的超类__init__。另一方面,QuestionVoteForm在超类__init__运行后手动设置error_class。
这似乎不太可能是问题的原因,因为在子类__init__例程中覆盖值,就像我们在QuestoinVoteForm中所做的那样,是一个非常常见的习语。不过,我们可以通过尝试在 Python shell 中演示使用自定义error_class设置的方法来检查是否这种细微差异会导致问题,如文档中所示,对于QuestionVoteForm:
kmt@lbox:/dj_projects/marketr$ python manage.py shell
Python 2.5.2 (r252:60911, Oct 5 2008, 19:24:49)
[GCC 4.3.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from survey.forms import QuestionVoteForm
>>> from survey.models import Question
>>> qvf = QuestionVoteForm(Question.objects.get(pk=1), data={})
在这里,我们已经创建了一个表单实例qvf,用于数据库中主键为1的问题。通过传递一个空的data字典,我们强制了一个没有answer值的表单提交的错误条件。文档显示,使用表单的as_p方法来显示这个表单应该显示使用表单的自定义错误类格式化的错误。我们可以检查QuestionVoteForm是否发生了这种情况:
>>> qvf.as_p()
u'Please select an answer below:\n<p><label for="id_answer_0">What is your favorite type of TV show?</label> <ul>\n<li><label for="id_answer_0"><input type="radio" id="id_answer_0" value="1" name="answer" /> Comedy</label></li>\n<li><label for="id_answer_1"><input type="radio" id="id_answer_1" value="2" name="answer" /> Drama</label></li>\n<li><label for="id_answer_2"><input type="radio" id="id_answer_2" value="3" name="answer" /> Reality</label></li>\n</ul></p>'
>>>
在那里,我们看到as_p方法确实使用了自定义错误类:错误消息周围没有 HTML 无序列表。因此,错误类已设置,并且在使用as_p等例行程序显示表单时使用。
这导致了文档显示和调查应用程序代码实际执行之间的第二个差异。survey/active_survey.html模板不使用as_p来显示表单。相反,它分别打印答案字段的标签,答案字段的错误,然后是答案字段本身。
{% extends "survey/base.html" %}
{% block content %}
<h1>{{ survey.title }}</h1>
<form method="post" action=".">
<div>
{% for qform in qforms %}
{{ qform.answer.label }}
{{ qform.answer.errors }}
{{ qform.answer }}
{% endfor %}
<button type="submit">Submit</button>
</div>
</form>
{% endblock content %}
这是否会导致自定义错误类不用于显示?你不会这样认为。尽管文档只显示自定义错误类与as_p一起使用,但在那里没有提到自定义错误类只被方便的显示方法如as_p使用。这样的限制将非常有限,因为方便的表单显示方法通常不适用于非平凡的表单。
似乎很明显,error_class属性的意图是覆盖错误显示,而不管表单以何种方式输出,但似乎并没有起作用。这是我们可能开始强烈怀疑 Django 中的一个错误,而不是应用程序代码中的某个错误或误用的地方。
搜索匹配的问题报告
接下来,我们要访问 Django 网站,看看是否有人报告了使用error_class的问题。从 Django 项目主页的Code链接(页面顶部横跨的链接的最右边)中选择Code链接,将显示 Django 代码跟踪器的主页面:
Django 项目使用 Trac,它提供了一个易于使用的基于 Web 的界面来跟踪错误和功能请求。使用 Trac,错误和功能请求是通过票务报告和跟踪的。Django 项目配置 Trac 的方式的具体细节,以及因此各种票务属性值的含义,可以在 Django 贡献文档页面上找到。特别是在这里找到的图表和描述:docs.djangoproject.com/en/dev/internals/contributing/#ticket-triage对于理解与票务相关的所有信息非常有帮助。
我们现在要做的是搜索 Django 项目票务,查找使用error_class的报告问题。一种方法是选择View Tickets选项卡并构建适当的搜索。当首次选择View Tickets时,默认情况下将显示列出所有未关闭票务的页面。例如:
用于生成报告的标准显示在标有Filters的框中。在这里,我们看到报告包括所有状态不是closed的票务,因为这是唯一未被选中的Status选项。为了获得更适合我们正在研究的报告,我们需要修改Filters框中的搜索标准。
首先,我们可以取消对票务状态的限制。我们对与error_class相关的所有报告感兴趣,而不管票务状态如何。我们可以通过点击包含约束的行的极右侧带有减号的框来删除现有的状态约束。
其次,我们需要为我们想要应用的搜索约束添加一个过滤器。为此,我们从添加过滤器下拉框中选择一个合适的选项。这个下拉框包含了我们可以搜索的票务属性的完整列表,比如报告人、所有者、状态和组件。这些属性中大多数对我们当前感兴趣的搜索不相关。在列表中最有可能找到我们要找的内容的是摘要。
票务摘要是问题的简要描述。我们希望这个摘要包含字符串error_class,以便包括我们遇到的问题的任何报告。在摘要上添加一个单一过滤器,并规定它包含字符串error_class,因此有望找到任何相关的票务。点击更新按钮以根据新标准刷新搜索结果,然后显示以下内容:
已经开启了三个包含error_class(或error class)的票务。其中两个已关闭,一个仍然打开(状态为新)。根据显示的摘要,这三个中,排在前面的一个听起来可能是我们遇到的与error_class有关的问题,而另外两个则似乎不太相关。
点击票号或摘要可以获取列出的问题的更多详细信息,这两者都是查看完整票务详情的链接。查看完整详情将允许我们验证它是否与我们所看到的情况相同,并了解更多有关关闭时间和原因的详细信息。在这种情况下,我们看到以下内容:
这个票务有一个相当长的历史——从开启到最后一次活动经过了两年。简要重现问题的指南确实让人觉得这可能正是我们遇到的与error_class有关的问题。在顶部附近的票号后面列出的已修复解决方案听起来令人鼓舞,但不幸的是,这个票务没有关于修复问题的代码更改以及何时进行的详细信息。在滚动到票务历史中添加的各种评论的最后,我们看到最近的几次更新如下:
2009 年 8 月,用户peter2108有兴趣通过提供补丁来帮助推动票务,包括修复问题的测试(通过阅读完整历史,最初提供的补丁中缺少测试是这个票务长时间开放的原因之一)。然后,在 2009 年 10 月 16 日,peter2108以已修复的解决方案关闭了票务。
一开始可能并不明显,但这种关闭票务的方式对于需要 Django 代码更改的票务来说并不典型。通常,当代码更改提交到 Django SVN 存储库时,票号会包含在提交消息中,并且相应的票务会自动更新,包括提交消息和更改集的链接。这个自动过程还会以已修复的解决方案关闭票务。这样就非常容易看到确切是哪个代码更改修复了问题,以及代码更改是何时进行的。
有时,自动过程无法正确运行,通常当发生这种情况时,有人会注意到并手动关闭票务,注明哪个代码更新修复了问题。但这也不是这里发生的情况。相反,看起来peter2108,他对看到错误修复感兴趣,只是注意到问题在某个时候消失了,并将票务标记为已修复。
我们可以猜测,基于同一个用户在 8 月份对问题进行了关闭,而在 10 月份又将问题关闭为已解决,修复可能是在 8 月 28 日至 10 月 16 日之间的某个时间点进入了代码库。然而,我们想要确定修复确切是何时进行的,这样我们就可以确定我们是否应该已经在我们运行的代码中拥有它,或者更新到最新版本是否会解决问题,或者修复是否仅在直接从 SVN 存储库中拉取的代码版本中可用。
回顾另外两个在摘要中提到error_class的票据,它们都无法帮助确定问题error_class究竟是何时修复的,因为它们描述的是完全不同的问题。那么,我们如何才能找到我们遇到的问题确切是何时修复的信息呢?对于这种情况,查看票据类型的搜索不够广泛,无法为我们提供所需的信息。幸运的是,有一种替代的搜索 Django 跟踪器的方法,我们可以使用它来找到缺失的信息。
搜索匹配问题报告的另一种方法
通过单击搜索选项卡而不是查看票据选项卡,可以找到这种替代搜索方式。这将显示一个带有单个文本输入框和三个复选框的页面,用于控制搜索位置:票据、变更集和Wiki。
此页面提供了一种更广泛和不太有针对性的搜索方式。除了搜索票据数据外,默认情况下还搜索变更集和 Wiki 文章。即使关闭这些选项,仅票据搜索也比查看票据下可能的搜索范围更广。此页面的票据搜索涵盖了所有票据评论和更新,这些在查看票据下无法搜索。
使用此页面进行搜索的一个好处是它可能会找到查看票据搜索无法找到的相关结果。使用此页面进行搜索的一个缺点是,它可能会找到大量无关的结果,具体取决于在文本框中输入了什么搜索词。如果发生这种情况,您可以通过在文本框中输入更多必须匹配的单词来进一步限制显示的结果,这可能有所帮助。然而,在这种情况下,搜索一个像error_class这样不常见的字符串不太可能产生大量的结果。
因此,输入error_class并单击搜索按钮会导致以下结果:
这次搜索产生的结果比查看票据搜索更多——12 个而不是 3 个。列出的第一个结果,票据**#12001**,与之前搜索找到的仍然打开的票据相同。先前搜索的其他结果也包含在完整列表中,只是在下面。但首先,我们可以看到一个变更集的结果,[11498],它在提交消息中提到了error_class,以及其关联的票据**#10968**。这个票据之前没有出现在我们尝试的原始搜索中,因为虽然它在完整描述中包含对error_class的引用,但字符串error_class不在票据摘要中。
单击**#10968票据的详细信息后,显示它是我们遇到的相同问题的重复报告,并且在我们找到的另一个票据#6138**中报告了该问题。通常,当这样的重复票据被打开时,它们会很快被关闭为重复,并引用描述问题的现有票据。
然而,如果没有人意识到新的票据是重复的,那么重复的票据可能会变成在检入代码库时引用的票据。这显然是在这种情况下发生的。我们可以在这个新票据的最后更新中看到自动生成的评论,当修复提交到 SVN 存储库时添加:
该评论中的变更集编号是变更集的详细描述的链接。单击它,我们会看到以下内容:
在这里,我们可以看到与此代码更改相关的所有详细信息:更改是何时进行的,由谁进行的,提交消息,更改的文件(或添加或删除的文件),更改的文件中的具体行以及这些更改是什么。对于我们现在正在研究的问题,大部分这些信息都不是我们真正需要了解的,但有时可能会派上用场。对于这个问题,我们想知道的是:哪个发布级别的代码包含了这个修复程序?我们将在下面考虑这个问题。
确定包含修复程序的发布版本
对于我们正在查看的特定情况,我们可以简单地根据日期判断,包含修复程序的第一个发布版本应该是 Django 1.1.1。在 Django 项目主页的网志上快速检查显示,Django 1.1 于 2009 年 7 月 29 日发布,Django 1.1.1 于 2009 年 10 月 9 日发布。在这些日期之间进行的所有错误修复应该包含在 1.1.1 版本中,因此 2009 年 9 月 11 日进行的修复应该包含在 Django 1.1.1 中。
有时事情可能不那么清楚。例如,我们可能不确定在发布当天进行的代码更改是否包含在发布中,或者是在发布后发生的。或者,我们可能不确定更改是被分类为错误修复还是新功能。对于这种情况,我们可以检查发布的修订号并将其与我们感兴趣的修订号进行比较。
Django 使用标记发布版本的标准子版本惯例;标记的发布版本可以在root/django/tags/releases下找到。我们可以通过首先选择浏览代码选项卡,然后依次选择每个路径组件来导航到此路径。以这种方式导航到 1.1.1 发布并在右上角单击修订日志会显示以下页面:
这表明 1.1.1 标记的发布版本是通过复制 1.1.X 发布分支创建的。创建标记的变更集是**[11612]**,高于我们感兴趣的变更集(11498),因此我们期望我们关心的修复程序在 1.1.X 发布中。
但等一下。查看变更集 11498 的详细信息时,更改的文件位于主干(例如django/trunk/django/forms/forms.py),而不是 1.1.X 发布分支django/branches/releases/1.1.X。如果发布是通过复制 1.1.X 分支创建的,但修复只是在主干上进行的,那么它真的包含在 1.1.1 发布中吗?
答案是肯定的。通过单击此页面上的链接转到 1.1.X 发布分支,为其选择修订日志,然后向下滚动到底部,显示 1.1.X 发布分支是作为主干的副本在修订版本 11500 时创建的,比我们感兴趣的修订版本 11498 晚两个修订版本。因此,当最初创建 1.1.X 分支时,它包含了我们正在寻找的修复程序。
您可能会想知道为什么 1.1.X 分支直到 2009 年 9 月 11 日之后才创建,而 1.1 版本在 7 月底就已经发布了。原因是因为一旦创建了发布分支,就必须在两个不同的地方应用错误修复程序:主干和最新的发布分支。这比只在一个地方(主干)应用它们稍微多一些工作。因此,发布分支的创建通常会在发布后的一段时间内延迟一段时间,以便更轻松地进行错误修复。
由于在不存在发布分支的时间内,不能对主干进行与新功能相关的更改,因为发布分支必须仅包含错误修复,而不包含新功能。不过,这通常不是问题,因为在发布后不久,很少进行功能工作。通常每个人都需要一些时间来休息,并首先决定哪些功能可能会进入下一个发布版本。一旦下一个发布版本的一些功能工作接近需要被检入的时候,那么就会创建上一个发布版本的发布分支。从那时起,功能工作被检入主干,而错误修复被检入主干和发布分支。
如果修复尚未发布呢?
在这里,我们很幸运地遇到了一个已经修复的问题,并且修复已经在官方发布的版本中可用。在遇到这样的问题时,应该很容易地选择更新到最新的微版本以获得修复。如前所述,始终建议安装正在使用的特定 major.minor 版本的最新微版本。
但是,如果我们想要的修复是在最新可用版本之后的某个时间点进行的呢?那么我们应该怎么办?简单的技术答案是简单地检出包含修复的最新级别的主干或发布分支,并使用该代码。特别是如果使用发布分支,就不必担心采用任何代码不稳定性,因为发布分支中的唯一更改是错误修复。
然而,这种简单的技术答案可能违反了关于仅运行“发布级别”代码的本地政策。如果你在这样的环境中工作,可能需要克服一些额外的障碍,以便使用尚未在官方版本中提供的修复。采取的最佳方法可能会由你所处理的确切政策、你遇到的问题的严重程度以及在你自己的代码中找到解决方法的能力等因素决定。
如果修复尚未提交呢?
有时在研究问题时,结果会显示问题已经被报告,但尚未修复。在这种情况下,如何最好地继续可能取决于你对参与和贡献到 Django 的兴趣程度,以及匹配问题报告与修复之间的接近程度。如何参与贡献到 Django 的细节超出了本文的范围,但本节提供了一些基于你的兴趣程度的广泛指导方针。如果你有兴趣贡献,Django 网站提供了如何贡献的详细信息,网址为:docs.djangoproject.com/en/dev/internals/contributing/。
如果你对尚未提交到代码库的代码不感兴趣,除了等待修复被提交之外,可能没有太多事情可做。唯一的例外是对于尚未被充分理解的问题。在这种情况下,你可能能够提供你遇到问题的具体细节,以帮助其他人更好地理解问题并开发修复方案。
如果你愿意尝试未提交的代码,你可能会更快地找到解决你遇到的问题的可行解决方案。在最好的情况下,你可能会发现与你遇到的问题匹配的票据已经附有一个有效的补丁。这是可能的,你所需要做的就是下载它并将其应用到你的 Django 代码副本中以解决问题。
然后,你需要决定是否能够并且愿意部署你的应用程序,使用一个已经应用了一些“自定义”修复的 Django 版本。如果不能,你可能想要帮助将工作补丁检查到代码库中,看看是否有任何遗漏的部分(比如测试)需要在修复被检查之前包含进去,如果有的话,提供这些遗漏的部分。然而,在某些情况下,没有任何遗漏,所需要的只是时间让修复进入代码库。
如果你找到一个有补丁的匹配票据,但它没有解决你所遇到的问题,那么这是有价值的信息,可以发布到票据上。不过,你可能首先要确保你的问题确实与你找到的票据中的问题相同。如果它确实是一个稍微不同的问题,那么为这个稍微不同的问题开一个新的票据可能更合适。
当你怀疑时,你可以随时在你认为匹配的票据中发布关于你所看到的问题以及现有补丁似乎无法解决它的信息。跟踪票据的其他人可能会提供反馈,告诉你你的问题是否相同,现有的补丁是否确实不太对,或者你是否真的在处理一个不同的问题。
在最坏的情况下,你可能会发现一个报告与你所经历的相同问题的票,但没有附加的补丁可供尝试。这对你来说并不是很有帮助,但却为你提供了最多的机会去贡献。如果你有时间并且愿意,你可以深入研究 Django 代码,看看是否能够提出一个补丁,然后将其发布到票据上,以帮助解决问题。
如果一个票已经关闭而没有修复呢?
有时在研究问题时,结果会出现一个匹配的报告(或多个报告),但没有进行任何修复而被关闭。在这种情况下可能会使用三种不同的解决方案:无效、worksforme 和 wontfix。如何最好地继续将取决于问题报告的具体情况以及用于关闭匹配问题票据的解决方案。
首先,无效的解决方案是非常广泛的。一个票可能因为很多不同的原因而被关闭为无效,包括:
-
描述的问题根本不是问题,而是报告者代码中的一些错误,或者对某些功能应该如何工作的误解。
-
描述的问题太模糊了。例如,如果一个票只提供了一个错误的回溯,但没有关于如何触发回溯的信息,那么没有人能够帮助跟踪并解决问题,所以它很可能会被关闭为无效。
-
描述的问题确实是一个问题,但根本原因是 Django 之外的一些代码。如果在 Django 代码中无法解决问题,那么票据很可能会被关闭为无效。
在你找到一个被关闭为无效的匹配票据时,你应该阅读票据关闭时所做的评论。在票据因缺乏关于问题的信息而关闭时,如果你可以提供一些需要的缺失数据来解决问题,重新打开票据可能是合适的。否则,如果你不理解关闭的解释,或者不同意关闭的原因,最好在邮件列表中开始讨论(在下一节中讨论),以获得更多关于如何最好地解决你遇到的问题的反馈。
worksforme 解决方案非常直接,它表示关闭工单的人无法重现报告的问题。它和 invalid 一样,可能是在原始问题报告中没有足够的信息来重现问题时使用的。缺少的信息可能是导致问题的代码的具体信息,或者问题发生的环境的具体信息(操作系统,Python 版本,部署细节)。如果您能够重现一个被关闭为 worksforme 的问题,并且能够提供缺失的细节,使其他人能够做同样的事情,那么您应该随时重新打开工单并提供这些信息。
wontfix 解决方案也很直接。通常只有核心贡献者会关闭 wontfix 工单,这表示核心团队已经决定不修复特定的问题(通常是一个功能请求,而不是一个错误)。如果您不同意 wontfix 的决定,或者认为在做出决定时没有考虑到所有适当的信息,那么您不会通过简单地重新打开工单来改变任何人的想法。相反,您需要在 django-developers 邮件列表上提出这个问题,并看看是否能够得到更广泛的开发社区的共识,以便推翻 wontfix 的决定。
追踪未报告的问题
有时在研究问题时,找不到匹配的报告。在这种情况下,最好的处理方式可能取决于您对您遇到的问题是否是 Django 中的错误有多确定。如果您非常确定问题出在 Django 中,您可以直接打开一个新的工单来报告它。如果您不太确定,最好先从社区中获得一些反馈。以下部分将描述在哪里提问,提供一些关于提问的好建议,并描述如何打开一个新的工单。
在哪里提问
在任何 Django 网站页面上点击社区链接会弹出以下内容:
这个页面的左侧提供了链接到博客文章的链接,这些文章是由讨论 Django 的人写的。阅读这些文章是了解使用 Django 的人群的一个好方法,但我们现在感兴趣的是这个页面的右侧。在这里,我们可以看到与 Django 社区其他成员直接互动的方式的链接。
列表中的第一个是**#django IRC 频道的链接。(IRC代表Internet Relay Chat**。)这个选项提供了一个聊天式界面,可以与其他 Django 用户进行互动交流。这是在您想要快速获得关于您想要询问或讨论的任何内容的反馈时的一个不错的选择。然而,在聊天界面中进行详细的编码讨论可能会有困难。对于这种情况,其中一个邮件列表可能是一个更好的选择。
有两个邮件列表,如下所示:django-users和django-developers。第一个用于讨论如何使用 Django,第二个用于讨论 Django 本身的开发。如果你遇到了一个问题,你认为,但不确定,是 Django 的问题,django-users 是发布有关该问题的问题的正确地方。Django 核心开发团队的成员会阅读并回答用户列表上的问题,并提供反馈,告知问题是否应该被提出为一个工单或者是否应该被带到开发者列表进行进一步讨论。
这两个邮件列表都托管在 Google 群组中。先前显示的每个组名称实际上都是一个链接,您可以单击该链接直接转到该组的 Google 群组页面。从那里,您可以查看该组中的最近讨论列表,并阅读可能感兴趣的任何主题。Google 群组还提供搜索功能,但不幸的是,该功能并不总是正常工作,因此从该组的页面中搜索可能不会产生有用的结果。
如果您决定要发布到其中一个组,您首先需要加入该组。这有助于减少发布到组的垃圾邮件,因为潜在的垃圾邮件发送者必须首先加入。然而,有很多潜在的垃圾邮件发送者确实加入并尝试向列表发送垃圾邮件。因此,还有一个额外的反垃圾邮件措施:新成员发送的帖子将通过审核。
这种反垃圾邮件措施意味着您发送到这些列表中的第一篇帖子可能需要一些时间才能出现,因为它必须由其中一位志愿者审核批准。通常情况下,这不会花费太长时间,但可能需要多达几个小时。通常情况下,一旦用户收到一个明显合法的第一篇帖子,他们的状态将被更新,以指示他们的帖子不需要经过审核,因此随后的帖子将立即出现在组中。
提出问题以获得良好答案的提示
一旦您决定发布问题,下一个任务将是以最有可能产生一些有用答案的方式撰写问题。本节提供了一些建议,说明如何做到这一点。
首先,要具体说明你正在做什么。如果您有一些代码的行为与您的期望不符,请直接包含代码,而不是用散文描述代码的功能。通常,实际使用的代码的详细细节是理解问题的关键,这些细节在代码的散文描述中很容易丢失。
然而,如果代码过长或过宽,无法在电子邮件界面中轻松阅读,因为它会自动换行长行,最好不要在帖子中包含它。理想情况下,在这种情况下,您应该能够将重新创建问题所需的代码剪切到一个可以在电子邮件中轻松阅读的可管理大小,并发布它。
请注意,如果您这样做,最好先验证剪裁版本的代码是否正确(例如没有任何语法错误)并且是否出现您所询问的问题。否则,您可能只会收到回复,报告发布的代码根本不起作用,或者不显示您描述的行为。
如果您无法将必要的代码剪裁到可管理的大小,要么是因为您没有时间,要么是因为剪裁代码会使问题消失,您可以尝试将代码发布在 dpaste.com 之类的地方,并在问题中包含一个链接。但是,最好将需要演示问题的代码保持尽可能短。随着您发布或指向的代码越来越长,邮件列表上的人越来越少,他们会花时间去理解问题并帮助您找到解决方案。
除了具体说明您正在使用的代码外,还要具体说明您正在做什么来触发错误行为。当您访问自己的应用程序 URL 之一时,您是否观察到问题?当您在管理应用程序中执行某些操作时?当您从manage.py shell 尝试某些操作时?这对您可能显而易见,但如果您详细说明您正在做什么,它确实有助于他人重现问题。
其次,具体说明发生了什么,以及你期望发生的是什么。"它不起作用"不是具体的。"它死了"也不是,"它给了我一个错误信息"也不是。给出"不起作用"的具体表现。当你期望 Y 时,浏览器页面显示 X?一个声明 XYZ 的错误信息?一个回溯?在最后一种情况下,在问题中包含完整的回溯,因为这为可能试图帮助的人提供了宝贵的调试线索。
第三,如果你在问题中提到你的预期行为是基于文档的,那么请具体说明你所指的是哪个文档。Django 有广泛的文档,包括指南和参考信息。阅读你的问题并搜索你所引用的文档的人可能会轻易地找到一个完全不同的部分,并且很难理解你的意思。如果你提供了问题文档的具体链接,那么误解的可能性就会降低。
你可能已经注意到,所有这些提示中都有一个共同的主题:具体。是的,提供具体信息需要更多的工作。但是一个具体的问题更有可能得到有用的答案,而不是一个不明确和模糊的问题。如果你省略了具体信息,偶尔会有人发布一个指导你解决问题的答案。然而,更有可能的是,一个模糊的问题要么得不到回应,要么得到要求具体信息的回应,要么得到完全误解问题的回应。
打开一个新的票证来报告问题
如果你遇到一个看起来是 Django 代码中未报告和未修复的错误,下一步就是为此问题打开一个票证。当你从 Django 首页点击Code后选择New Ticket选项卡时,这个过程就变得非常直观了。
请确保阅读首先阅读列表。该列表中的许多信息在本章的早些部分已经涵盖了,但并非全部。特别是,最后一项指出了如何标记提交的代码片段或回溯,以便它们能够正确格式化。该注释包括最常被忽略的一种标记类型,并指向了关于如何特殊格式化文本的完整文档。请注意,你可以通过选择底部的预览按钮来检查格式化的效果——在按下提交之前尝试预览是一个很好的主意。
请注意,Django Trac 安装确实允许匿名提交和更新票证。然而,它也使用 Akismet 垃圾邮件过滤服务,这项服务有时会拒绝非垃圾邮件的提交。正如大黄框中所指出的,避免这种情况的最简单方法是注册一个账户(页面上的文字是一个链接,可以跳转到注册页面)。
在打开一个新的票证时,填写最重要的部分是简短的摘要和完整的描述。在简短的摘要中,尽量包含关键术语,这样新的票证就会在遇到相同问题的人的可能搜索中显示出来。在完整的描述中,之前关于具体性的所有建议都再次适用。如果你在邮件列表的讨论后得出结论认为需要打开一个票证,那么在问题中包含该讨论的链接是有帮助的。然而,在票证描述中也包含关于问题的基本信息也是很好的。
在票据属性中的信息中,您可能不需要更改任何默认值,除了版本(如果您使用的版本与显示的版本不同)和有补丁(如果您将附加修复问题的补丁)。您可以尝试从列表中猜测正确的组件并包含一些适当的关键字,但这并非必要。
同样,您可以将里程碑设置为下一个发布版本,尽管这并不会使有人更有可能尽快解决问题。该字段通常只在发布的最后阶段密切关注,以记录哪些错误绝对必须在发布之前修复。
提交票据后,如果您使用包含电子邮件地址的登录,或在标有您的电子邮件或用户名的字段中指定了电子邮件地址,则票据的更新将自动发送到指定的电子邮件地址。因此,如果有人向票据添加评论,您将收到通知。这种情况的一个令人讨厌的例外是由于对代码库的提交而产生的自动生成的更新:这不会生成发送给票据报告者的电子邮件。因此,当票据被关闭为已修复时,您不一定会收到通知,而是必须手动从网站上检查其状态。
总结
现在我们讨论了在先前介绍的调试技术都未能成功解决某些问题时该怎么办。在本章中,我们:
-
遇到了一个存在于 Django 1.1 中的错误,导致一些调查应用代码无法按预期行为
-
走过了追踪问题的验证过程,发现问题是由 Django 而不是调查代码引起的
-
看到在 Django 代码跟踪器中搜索揭示了问题是一个已在 Django 1.1.1 中修复的错误,这为问题提供了一个简单的解决方案
-
讨论了当问题被追踪到尚未可用或在官方发布中不可用的修复程序时如何继续的选项
-
描述了存在的各种社区资源,用于询问行为似乎令人困惑,但似乎尚未被报告为错误的问题
-
讨论了撰写问题的提示,以便获得所需的有益回应
-
描述了在 Django 代码中报告问题时打开新票的过程
在下一章中,我们将进入 Django 应用程序开发的最后阶段:转向生产。
第十一章:当是时候上线:转入生产环境
我们将在测试和调试 Django 应用程序的主题上涵盖的最后一个主题是转入生产环境。当应用程序代码全部编写、完全测试和调试完成时,就是设置生产 Web 服务器并使应用程序对真实用户可访问的时候了。由于应用程序在开发过程中已经经过了全面的测试和调试,这应该是直截了当的,对吗?不幸的是,情况并非总是如此。生产 Web 服务器环境与 Django 开发服务器环境之间存在许多差异。这些差异可能在转入生产过程中引发问题。在本章中,我们将看到其中一些差异是什么,它们可能引起什么类型的问题,以及如何克服它们。具体来说,我们将:
-
配置一个带有
mod_wsgi的 Apache Web 服务器来运行示例marketr项目。 -
在开发 Apache 配置过程中遇到了一些问题。针对每个问题,我们将看看如何诊断和解决这些问题。
-
对在 Apache 下运行的应用程序进行功能性压力测试,以确保它在负载下能够正确运行。
-
修复功能性压力测试中暴露的任何代码错误。
-
讨论在开发过程中使用 Apache 和
mod_wsgi的可能性。
开发 Apache/mod_wsgi 配置
通常,转入生产环境将涉及在与开发时使用的不同机器上运行代码。生产服务器可能是专用硬件,也可能是从托管提供商那里获得的资源。无论哪种情况,它通常与开发人员编写代码时使用的机器完全分开。生产服务器需要安装任何先决条件包(例如,对于我们的示例项目,需要安装 Django 和 matplotlib)。此外,应用项目代码的副本通常需要从版本控制系统中提取,并放置在生产服务器上。
为了简化本章,我们将在与开发代码相同的机器上配置生产 Web 服务器。这将使我们能够跳过一些在实际转入生产过程中涉及的复杂性,同时仍然体验到在生产部署过程中可能出现的许多问题。在很大程度上,我们将跳过这样做时遇到的问题并不是特定于 Django 的,而是在将任何类型的应用程序从开发转入生产时需要处理的常见问题。我们将遇到的问题往往更具体于 Django。
将要开发的示例部署环境是使用mod_wsgi的 Apache,这是目前推荐的部署 Django 应用程序的环境。WSGI代表Web Server Gateway Interface。WSGI 是 Python 的标准规范,定义了 Web 服务器(例如 Apache)和用 Python 编写的 Web 应用程序或框架(例如 Django)之间的接口。
基本的 Apache Web 服务器不支持 WSGI。然而,Apache 的模块化结构允许通过插件模块提供此支持。因此,WSGI 的 Web 服务器端支持由 Graham Dumpleton 编写并积极维护的mod_wsgi提供。Django 本身确实实现了 WSGI 规范的应用程序端。因此,在mod_wsgi和 Django 之间不需要任何额外的适配器模块。
注意
在开发mod_wsgi之前,Apache 的mod_python模块是 Django 推荐的部署环境。尽管mod_python仍然可用且仍然被广泛使用,但其最近的发布已经超过三年。当前的源代码需要一个补丁才能与最新的 Apache 2.2.X 版本编译。未来,由于 Apache API 的更改,将需要更广泛的更改,但没有活跃的mod_python开发人员来进行这些更改。鉴于mod_python开发目前的停滞状态,我认为它现在不适合用于 Django 应用程序的部署。因此,这里不涵盖配置它的具体内容。如果出于某种原因您必须使用mod_python,那么在本章中遇到的许多问题也适用于mod_python,并且配置mod_python的具体内容仍包含在 Django 文档中。
Apache 和mod_wsgi都可以在各种不同的平台上轻松获取和安装。这里不会涵盖这些安装的细节。一般来说,使用机器的常规软件包管理服务来安装这些软件包可能是最简单的方法。如果这不可能,可以在网上找到有关下载和安装 Apache 的详细信息,网址为httpd.apache.org/,同样的信息也可以在code.google.com/p/modwsgi/找到mod_wsgi的信息。
本章展示的示例配置的开发机器运行 Ubuntu,这是 Linux 的基于 Debian 的版本。这种 Linux 的配置结构可能与您自己机器上使用的结构不匹配。然而,配置结构并不重要,重要的是配置中包含的 Apache 指令。如果您的机器不遵循 Debian 结构,您可以简单地将这里显示的指令放在主 Apache 配置文件中,通常命名为httpd.conf。
在 Apache 下运行 WSGI 客户端应用程序的配置有两个部分,首先是一个 Python WSGI 脚本,它设置环境并标识将处理请求的 WSGI 客户端应用程序。其次是控制mod_wsgi操作并将特定 URL 路径的请求指向mod_wsgi的 Apache 配置指令。接下来将讨论为 Django marketr项目创建这两部分。
创建marketr项目的 WSGI 脚本。
Django 项目的 WSGI 脚本有三个责任。首先,它必须设置 Python 路径,包括 Django 项目所需但不在常规系统路径上的任何路径。在我们的情况下,martketr项目本身的路径将需要添加到 Python 路径中。项目使用的所有其他先决条件代码都已安装,因此它会自动在 Python site-packages 目录下找到。
其次,WSGI 脚本必须在环境中设置DJANGO_SETTINGS_MODULE变量,指向适当的设置模块。在我们的情况下,它需要设置为指向/dj_projects/marketr中的settings.py文件。
第三,WSGI 脚本必须将变量application设置为实现 WSGI 接口的可调用实例。对于 Django,这个接口由django.core.handlers.wsgi.WSGIHandler提供,因此marketr项目的脚本可以简单地将application设置为该类的实例。这里没有特定于marketr项目的内容——这部分 WSGI 脚本对所有 Django 项目都是相同的。
这个脚本应该放在哪里?将其直接放在/dj_projects/marketr中似乎是很自然的,与settings.py和urls.py文件一起,因为它们都是项目级文件。然而,正如mod_wsgi文档中所提到的,这将是一个不好的选择。Apache 需要配置以允许访问包含 WSGI 脚本的目录中的文件。因此,最好将 WSGI 脚本保存在与不应对网站用户可访问的任何代码文件分开的目录中。(特别是包含settings.py的目录,绝对不应该配置为对网站客户端可访问,因为它可能包含诸如数据库密码之类的敏感信息。)
因此,我们将在/dj_projects/marketr内创建一个名为apache的新目录,用于保存在 Apache 下运行项目相关的所有文件。在apache目录下,我们将创建一个wsgi目录,用于保存marketr项目的 WSGI 脚本,我们将其命名为marketr.wsgi。根据此脚本的前面提到的三个职责,实现/dj_projects/marketr/apache/wsgi/marketr.wsgi脚本的第一步可能是:
import os, sys
sys.path = ['/dj_projects/marketr', ] + sys.path
os.environ['DJANGO_SETTINGS_MODULE'] = 'marketr.settings'
import django.core.handlers.wsgi
application = django.core.handlers.wsgi.WSGIHandler()
此代码将marketr项目目录添加到 Python 系统路径的最前面,将DJANGO_SETTINGS_MODULE环境变量设置为marketr.settings,并将application设置为实现 WSGI 应用程序接口的 Django 提供的可调用实例。当mod_wsgi被调用以响应已映射到此脚本的 URL 路径时,它将使用正确设置的环境调用适当的 Django 代码,以便 Django 能够处理请求。因此,下一步是开发 Apache 配置,将请求适当地路由到mod_wsgi和此脚本。
为 marketr 项目创建 Apache 虚拟主机
为了将 Django 项目与您可能已经使用 Apache 的其他任何内容隔离开来,我们将使用绑定到端口 8080 的 Apache VirtualHost来进行 Django 配置。以下指令指示 Apache 监听端口 8080 的请求,并定义一个虚拟主机来处理这些请求:
Listen 8080
<VirtualHost *:8080>
WSGIScriptAlias / /dj_projects/marketr/apache/wsgi/marketr.wsgi
WSGIDaemonProcess marketr
WSGIProcessGroup marketr
# Possible values include: debug, info, notice, warn, error, crit,
# alert, emerg.
LogLevel debug
ErrorLog /dj_projects/marketr/apache/logs/error.log
CustomLog /dj_projects/marketr/apache/logs/access.log combined
</VirtualHost>
请注意,这绝不是一个完整的 Apache 配置,而是需要添加到现有(或已发货的示例)配置中以支持处理定向到端口 8080 的marketr项目请求的内容。在VirtualHost容器内有三个指令控制mod_wsgi的行为,另外三个指令将影响此虚拟主机的日志处理方式。
第一个指令WSGIScriptAlias很简单。它将与其第一个参数匹配的所有请求映射到其第二个参数中指定的 WSGI 脚本,即/dj_projects/marketr/apache/wsgi/marketr.wsgi。此指令的效果将是将此虚拟主机的所有请求路由到前面部分定义的marketrWSGI 脚本。
接下来的两个指令,WSGIDaemonProcess和WSGIProcessGroup,指示mod_wsgi将此虚拟主机的请求路由到一个独立的进程组,与用于处理请求的正常 Apache 子进程不同。这被称为以守护程序模式运行mod_wsgi。相比之下,让mod_wsgi使用正常的 Apache 子进程被称为以嵌入模式运行。
通常情况下,以守护程序模式运行更可取(有关详细信息,请参阅mod_wsgi文档),但在 Windows 上运行 Apache 时不支持此模式。因此,如果您在 Windows 机器上使用 Apache 服务器,则需要从配置中省略这两个指令。
在所示的指令中,WSGIDaemonProcess指令定义了一个名为marketr的进程组。这个指令支持几个额外的参数,可以用来控制,例如,组中的进程数量,每个进程中的线程数量,以及进程的用户和组。这里没有指定这些参数,所以mod_wsgi将使用其默认值。WSGIProcessGroup指令将先前定义的marketr组命名为处理这个虚拟主机请求的组。
下一个指令,LogLevel debug,将日志设置为最详细的设置。在生产环境中,更典型的设置可能是warn,但是当刚开始设置某些东西时,通常有必要让代码记录尽可能多的信息,所以我们将在这里使用debug。
最后两个指令,ErrorLog和CustomLog,为这个虚拟主机定义了错误和访问日志,与主要的 Apache 错误和访问日志不同。这可以方便地将与新项目相关的日志信息与 Apache 可能处理的其他流量隔离开来。在这种情况下,我们已经指示 Apache 将日志放置在/dj_projects/marketr/apache目录下的logs目录中。
激活新的 Apache 配置
上一节的配置指令应该放在哪里?正如前面所述,答案取决于 Apache 在您的机器上的配置细节。对于由单个httpd.conf文件组成的 Apache 配置,您可以简单地将指令放在该文件的末尾。尽管这对于更结构化的配置也可能有效,但最好避免混淆并使用提供的结构。因此,本节将描述如何将先前列出的定义集成到基于 Debian 的配置中,因为这是示例项目所使用的机器类型。
对于基于 Debian 的 Apache 配置,Listen指令应放置在/etc/apache2/ports.conf中。VirtualHost指令及其所有内容应放置在/etc/apache2/sites-available下的一个文件中。但是,在这个例子中,虚拟主机配置已放置在一个名为/dj_projects/marketr/apache/conf/marketr的文件中,以便/dj_projects目录可以包含项目的完整配置信息。我们可以通过为其创建一个符号链接,使这个文件也出现在sites-available目录中:
kmt@lbox:/etc/apache2/sites-available$ sudo ln -s /dj_projects/marketr/apache/conf/marketr
请注意,一般用户无法在/etc/apache2/sites-available下创建或修改文件,因此需要使用sudo命令以超级用户的身份执行所请求的命令。这对于所有修改 Apache 配置或控制其操作的命令都是必要的。
一旦包含虚拟主机配置的文件放置在sites-available中,就可以使用a2ensite命令启用新站点:
kmt@lbox:/etc/apache2/sites-available$ sudo a2ensite marketr
Enabling site marketr.
Run '/etc/init.d/apache2 reload' to activate new configuration!
a2ensite命令在/etc/apache2/sites-enabled目录中为sites-available目录中指定的文件创建一个符号链接。还有一个伴随命令a2dissite,它通过在sites-enabled中删除指定文件的符号链接来禁用站点。(请注意,如果愿意,您也可以手动管理符号链接,而不使用这些命令。)
正如a2ensite的输出所示,需要重新加载 Apache 才能使新的站点配置生效。在这种情况下,由于添加了Listen指令,需要完全重新启动 Apache。这可以通过运行/etc/init.d/apache2命令并指定restart作为参数来完成。当我们尝试这样做时,响应如下:
屏幕右侧的 [fail] 看起来不太好。显然在重新启动期间出了一些问题,但是是什么呢?答案在于重新启动 Apache 时使用的命令的输出中找不到,它只报告成功或失败。相反,Apache 错误日志包含了失败原因的详细信息。此外,对于与服务器启动相关的失败,主要的 Apache 错误日志可能包含详细信息,而不是特定于站点的错误日志。在这台机器上,主要的 Apache 错误日志文件是/var/log/apache2/error.log。查看该文件的末尾,我们找到了以下内容:
(2)No such file or directory: apache2: could not open error log file /dj_projects/marketr/apache/logs/error.log.
Unable to open logs
问题在于新的虚拟主机配置指定了一个不存在的错误日志文件目录。Apache 不会自动创建指定的目录,因此我们需要手动创建它。这样做并再次尝试重新启动 Apache 会产生更好的结果:
[ OK ] 确实看起来比 [fail] 好得多;显然这一次 Apache 能够成功启动。我们现在已经到了拥有有效 Apache 配置的地步,但可能还有一些工作要做才能获得一个可用的配置,接下来我们将看到。
调试新的 Apache 配置
下一个测试是看 Apache 是否能成功处理发送到新虚拟主机端口的请求。为了做到这一点,让我们尝试从 Web 浏览器中检索项目根(主页)。结果看起来不太好:
现在可能出了什么问题?在这种情况下,主要的 Apache 错误日志对于错误的原因是沉默的。相反,是为marketr虚拟站点配置的错误日志提供了问题的指示。检查该文件,我们看到/dj_projects/marketr/apache/logs/error.log的完整内容现在是:
[Mon Dec 21 17:59:01 2009] [info] mod_wsgi (pid=18106): Attach interpreter ''.
[Mon Dec 21 17:59:01 2009] [info] mod_wsgi (pid=18106): Enable monitor thread in process 'marketr'.
[Mon Dec 21 17:59:01 2009] [debug] mod_wsgi.c(8301): mod_wsgi (pid=18106): Deadlock timeout is 300\.
[Mon Dec 21 17:59:01 2009] [debug] mod_wsgi.c(8304): mod_wsgi (pid=18106): Inactivity timeout is 0\.
[Mon Dec 21 17:59:01 2009] [info] mod_wsgi (pid=18106): Enable deadlock thread in process 'marketr'.
[Mon Dec 21 17:59:01 2009] [debug] mod_wsgi.c(8449): mod_wsgi (pid=18106): Starting 15 threads in daemon process 'marketr'.
[Mon Dec 21 17:59:01 2009] [debug] mod_wsgi.c(8455): mod_wsgi (pid=18106): Starting thread 1 in daemon process 'marketr'.
[Mon Dec 21 17:59:01 2009] [debug] mod_wsgi.c(8455): mod_wsgi (pid=18106): Starting thread 2 in daemon process 'marketr'.
[… identical messages for threads 3 through 13 deleted …]
(pid=18106): Starting thread 14 in daemon process 'marketr'.
[Mon Dec 21 17:59:01 2009] [debug] mod_wsgi.c(8455): mod_wsgi (pid=18106): Starting thread 15 in daemon process 'marketr'.
[Mon Dec 21 17:59:45 2009] [error] [client 127.0.0.1] client denied by server configuration: /dj_projects/marketr/apache/wsgi/marketr.wsgi
除了最后一个之外,这些消息都没有指示问题。相反,它们是由mod_wsgi记录的信息和调试级别消息,根据虚拟主机配置中LogLevel debug的设置。这些消息显示mod_wsgi报告了它正在使用的各种值(死锁超时,不活动超时),并显示mod_wsgi在守护进程marketr中启动了 15 个线程。一切看起来都很好,直到最后一行,这是一个错误级别的消息。
最后一条消息的具体内容并没有比 Web 浏览器显示的光秃秃的 Forbidden 更有帮助。消息确实表明marketr.wsgi脚本涉及其中,并且请求被 服务器配置拒绝。在这种情况下,问题不在于文件不存在,而是服务器已经配置为不允许访问它。
这个特定问题的原因在这台机器上的 Apache 配置的其他地方,这是一个问题,根据您的整体 Apache 配置,您可能会遇到或者不会遇到。问题在于这台机器的 Apache 配置已经设置为拒绝访问除了明确启用访问的所有目录中的文件。从安全的角度来看,这种类型的配置是好的,但它确实使配置变得有点更加费力。在这种情况下,需要的是一个Directory块,允许访问包含marketr.wsgi脚本的目录中的文件:
<Directory /dj_projects/marketr/apache/wsgi>
Order allow,deny
Allow from all
</Directory>
Apache 三遍访问控制系统的细节超出了本书的范围;如果您感兴趣,Apache 文档详细描述了这个过程。对于我们的目的,值得注意的是这个Directory块允许所有客户端访问/dj_projets/marketr/apache/wsgi中的文件,这应该是可以接受的,并足以解决浏览器对marketr项目主页最初返回的 Forbidden。
Directory块应放在marketr项目的VirtualHost块内。更改配置需要重新启动 Apache,之后我们可以再次尝试访问项目主页。这次我们看到以下内容:
好消息是我们已经解决了Forbidden错误。坏消息是我们并没有走得更远。再次返回到浏览器的页面对于调试问题没有什么用,而网站的错误日志记录了问题的详细信息。这次在文件的末尾我们发现:
[Mon Dec 21 18:05:43 2009] [debug] mod_wsgi.c(8455): mod_wsgi (pid=18441): Starting thread 15 in daemon process 'marketr'.
[Mon Dec 21 18:05:49 2009] [info] mod_wsgi (pid=18441): Create interpreter 'localhost.localdomain:8080|'.
[Mon Dec 21 18:05:49 2009] [info] [client 127.0.0.1] mod_wsgi (pid=18441, process='marketr', application='localhost.localdomain:8080|'): Loading WSGI script '/dj_projects/marketr/apache/wsgi/marketr.wsgi'.
[Mon Dec 21 18:05:49 2009] [error] [client 127.0.0.1] mod_wsgi (pid=18441): Exception occurred processing WSGI script '/dj_projects/marketr/apache/wsgi/marketr.wsgi'.
[Mon Dec 21 18:05:49 2009] [error] [client 127.0.0.1] Traceback (most recent call last):
[Mon Dec 21 18:05:49 2009] [error] [client 127.0.0.1] File "/usr/lib/python2.5/site-packages/django/core/handlers/wsgi.py", line 230, in __call__
[Mon Dec 21 18:05:49 2009] [error] [client 127.0.0.1] self.load_middleware()
[Mon Dec 21 18:05:49 2009] [error] [client 127.0.0.1] File "/usr/lib/python2.5/site-packages/django/core/handlers/base.py", line 33, in load_middleware
[Mon Dec 21 18:05:49 2009] [error] [client 127.0.0.1] for middleware_path in settings.MIDDLEWARE_CLASSES:
[Mon Dec 21 18:05:49 2009] [error] [client 127.0.0.1] File "/usr/lib/python2.5/site-packages/django/utils/functional.py", line 269, in __getattr__
[Mon Dec 21 18:05:49 2009] [error] [client 127.0.0.1] self._setup()
[Mon Dec 21 18:05:49 2009] [error] [client 127.0.0.1] File "/usr/lib/python2.5/site-packages/django/conf/__init__.py", line 40, in _setup
[Mon Dec 21 18:05:49 2009] [error] [client 127.0.0.1] self._wrapped = Settings(settings_module)
[Mon Dec 21 18:05:49 2009] [error] [client 127.0.0.1] File "/usr/lib/python2.5/site-packages/django/conf/__init__.py", line 75, in __init__
[Mon Dec 21 18:05:49 2009] [error] [client 127.0.0.1] raise ImportError, "Could not import settings '%s' (Is it on sys.path? Does it have syntax errors?): %s" % (self.SETTINGS_MODULE, e)
[Mon Dec 21 18:05:49 2009] [error] [client 127.0.0.1] ImportError: Could not import settings 'marketr.settings' (Is it on sys.path? Does it have syntax errors?): No module named marketr.settings
显然,marketr.wsgi脚本这次确实被使用了,因为回溯显示 Django 代码已被调用。但是环境设置并不完全正确,因为 Django 无法导入指定的marketr.settings设置模块。这是一个常见的错误,几乎总是由两种情况之一引起的:要么 Python 路径没有正确设置,要么 Apache 进程运行的用户没有读取设置文件(以及包含它的目录)的权限。
在这种情况下,快速检查/dj_projects/marketr目录及其文件的权限显示它们是可读的:
kmt@lbox:/dj_projects/marketr$ ls -la
total 56
drwxr-xr-x 7 kmt kmt 4096 2009-12-21 18:42 .
drwxr-Sr-x 3 kmt kmt 4096 2009-12-20 09:46 ..
drwxr-xr-x 5 kmt kmt 4096 2009-12-21 17:58 apache
drwxr-xr-x 2 kmt kmt 4096 2009-11-22 11:40 coverage_html
drwxr-xr-x 4 kmt kmt 4096 2009-12-20 09:50 gen_utils
-rw-r--r-- 1 kmt kmt 0 2009-11-22 11:40 __init__.py
-rw-r--r-- 1 kmt kmt 130 2009-12-20 09:49 __init__.pyc
-rwxr-xr-x 1 kmt kmt 546 2009-11-22 11:40 manage.py
-rwxr--r-- 1 kmt kmt 5800 2009-12-20 09:50 settings.py
-rw-r--r-- 1 kmt kmt 2675 2009-12-20 09:50 settings.pyc
drwxr-xr-x 3 kmt kmt 4096 2009-12-20 09:50 site_media
drwxr-xr-x 5 kmt kmt 4096 2009-12-20 19:42 survey
-rwxr--r-- 1 kmt kmt 734 2009-11-22 11:40 urls.py
-rw-r--r-- 1 kmt kmt 619 2009-12-20 09:50 urls.pyc
因此,问题似乎不太可能与 Web 服务器进程访问settings.py文件的能力有关。但是,请注意,如果您运行的是使用安全增强内核(SELinux 内核)的 Linux 版本,则ls -l显示的权限信息可能会误导。这个内核有一个复杂的文件访问控制结构,需要额外的配置(超出本书的范围)才能允许 Web 服务器进程访问其自己指定区域之外的文件。
不过,在这种情况下,机器并没有运行 SELinux 内核,并且权限信息显示任何进程都可以读取settings.py文件。因此,问题很可能在路径设置中。请回忆一下marketr.wsgi脚本中的路径和设置规范:
sys.path = ['/dj_projects/marketr', ] + sys.path
os.environ['DJANGO_SETTINGS_MODULE'] = 'marketr.settings'
这个路径无法导入指定为marketr.settings的设置文件,因为路径和模块规范中的marketr部分都被重复了。Python 在尝试找到模块并使用路径上的第一个元素时,将尝试找到一个名为/dj_projects/marketr/marketr/settings.py的文件。这将失败,因为实际文件是/dj_projects/marketr/settings.py。除非/dj_projects单独在sys.path上,否则 Python 将无法加载marketr.settings。
因此,一个解决方法是在路径设置中包含/dj_projects。
sys.path = ['/dj_projects/marketr', '/dj_projects', ] + sys.path
不过,需要为一个项目添加两个不同的项目路径似乎有点奇怪。这两个都真的必要吗?第一个是必要的,因为在调查应用程序代码中,例如,我们使用了以下形式的导入:
from survey.models import Survey
from survey.forms import QuestionVoteForm
由于这些导入中没有包含marketr,因此必须在 Python 路径的一个元素中包含它。在运行开发服务器时,/dj_projects/marketr目录是当前路径,自动包含在 Python 路径中,因此这些导入有效。在 Apache 下运行时,必须在路径中包含/dj_projects/marketr才能使这些导入工作。
或者,我们可以更改survey和gen_utils应用程序中的所有导入,使用以下形式:
from marketr.survey.models import Survey
from marketr.survey.forms import QuestionVoteForm
然而,这种方法将这些应用程序紧密地绑定到marketr项目,使得在该项目之外重新使用它们变得更加困难。我认为最好的做法是使应用程序独立,不在它们的导入中包含包含项目的名称。
那么/dj_projects呢?是否真的需要在路径中包含它?我们是否可以通过将设置模块指定为简单的settings而不是marketr.settings来消除需要在路径中包含它的需要?是的,这将使我们摆脱特定的错误,但当处理设置文件中的ROOT_URLCONF值时,我们很快会遇到另一个类似的错误。ROOT_URLCONF也在其规范中包括marketr:
ROOT_URLCONF = 'marketr.urls'
我们也可以更改它,并希望这是最后一个问题,但最好的方法可能是在 Web 服务器下运行时简单地包括/dj_projects在路径中。
您可能会想知道在开发服务器下运行时如何将/dj_projects包含在路径中,因为当前目录的父目录通常不包含在 Python 路径中,就像当前目录一样。答案是,开发服务器的设置代码将项目目录的父目录放在 Python 路径中。对于刚开始学习 Python 的人来说,这可能有所帮助,但长远来看,这往往会引起混乱,因为对于不是刚开始学习 Python 的人来说,这是令人惊讶的行为。
然而,要从这一点继续,我们只需在 Python 路径中包括/dj_projects以及/dj_projects/marketr,如前所示。请注意,在守护程序模式下运行mod_wsgi时,不需要重新加载或重新启动 Apache 即可使其获取 WSGI 脚本的更改。更改 WSGI 脚本本身足以导致mod_wsgi自动重新启动其守护进程。因此,我们只需要保存修改后的文件,然后再次尝试访问项目主页。这次我们看到以下内容:
我们再次有好消息和坏消息。我们确实取得了进展,Django 代码运行良好,足以返回调试页面,这是令人鼓舞的,比起不得不在 Apache 错误日志中搜索问题,这更容易处理。不幸的是,我们得到调试页面而不是项目主页意味着在 Web 服务器下运行时环境仍然存在一些问题。
这次异常信息表明matplotlib代码需要对其配置数据的目录具有写访问权限。它显然尝试创建一个名为/var/www/.matplotlib的目录,但失败了。消息表明,如果设置一个名为MPLCONFIGDIR的环境变量指向一个可写目录,我们可能会解决这个问题。我们当然可以在marketr.wsgi脚本中设置这个环境变量,就像设置DJANGO_SETTINGS_MODULE环境变量一样:
os.environ['DJANGO_SETTINGS_MODULE'] = 'marketr.settings'
os.environ['MPLCONFIGDIR'] = '/dj_projects/marketr/apache/.matplotlib'
我们还需要创建指定的目录,并使 Web 服务器进程可以对其进行写操作。最简单的方法是将目录的所有者更改为 Web 服务器进程运行的用户,这台机器上的用户是www-data:
kmt@lbox:/dj_projects/marketr/apache$ mkdir .matplotlib
kmt@lbox:/dj_projects/marketr/apache$ sudo chown www-data .matplotlib/
或者,虚拟主机配置中的WSGIDaemonProcess指令可以更改为指定不同的用户。但是,默认情况下,唯一具有对/dj_projects目录下目录的写访问权限的用户是我的用户kmt,我宁愿不让 Web 服务器进程以写访问权限运行我的所有文件。因此,更容易的方法是让 Web 服务器继续以www-data运行,并明确允许它根据需要访问目录。请注意,如果您使用 SQLite 作为数据库,还需要设置数据库文件的权限,以便 Apache 进程可以读取和写入它。
我们已经解决了最后一个问题吗?保存更改后的marketr.wsgi文件并重试项目主页,会出现以下内容:
最后成功,但是有点。主页上没有显示调查,因为已经过了足够长的时间,我们一直在处理的那个已关闭的survey现在已经关闭了太长时间,无法列出。因此,在主页上没有太多有趣的东西可看。测试的下一个自然步骤是转到管理员应用程序,并更改调查的closes日期,以便它出现在主页上。尝试这样做会显示一些我们尚未设置的配置,接下来将讨论。
配置 Apache 以提供静态文件
尝试在 Apache 下访问管理员应用程序,我们得到:
这看起来很像我们的示例项目页面,没有任何自定义样式。但是,与我们的示例项目不同,管理员应用程序确实有它使用的样式表,在运行开发服务器时正确加载。这是由开发服务器中的专用代码完成的。在 Apache 下运行时,我们需要配置它(或其他 Web 服务器)来提供管理员应用程序的静态文件。
我们该如何做呢?所有管理员的静态文件都将使用相同的前缀引用,由settings.py中的ADMIN_MEDIA_PREFIX指定。此设置的默认值为/media/。因此,我们需要指示 Apache 直接从管理员的媒体目录树中提供带有此前缀的文件,而不是将请求路由到mod_wsgi和我们的 Django 项目代码。
实现这一目标的 Apache 指令是(请注意,下面的Alias和Directory行由于页面宽度限制而被拆分,这些指令需要放在 Apache 配置文件中的单行上):
Alias /media /usr/lib/python2.5/site-packages/django/contrib/admin/media/
<Directory /usr/lib/python2.5/site-packages/django/contrib/admin/media>
Order allow,deny
Allow from all
</Directory>
第一个指令Alias设置了从以/media开头的 URL 路径到实际文件的映射,这些文件位于(在此计算机上)/usr/lib/python2.5/site-packages/django/contrib/admin/media/下。接下来的Directory块指示 Apache 允许所有客户端访问管理员媒体所在的目录中的文件。与marketr.wsgi脚本的Directory块一样,只有在您的 Apache 配置已经设置为默认拒绝访问所有目录时才需要这样做。
这些指令应该放在marketr项目虚拟主机的VirtualHost块中。然后需要重新加载 Apache 以识别配置更改。在浏览器中重新加载管理员页面,然后会出现带有正确自定义样式的页面:
请注意,不仅管理员有静态文件。在第九章中,当你甚至不知道要记录什么时:使用调试器,我们将一些静态文件的使用添加到了marketr项目中。具体来说,由 matplotlib 生成的图像文件以显示调查结果被作为静态文件提供。与管理员媒体文件不同,这些文件不会被开发服务器自动提供,因此我们不得不在marketr项目的urls.py文件中为它们添加一个条目,指定它们由 Django 静态服务器视图提供:
(r'^site_media/(.*)$', 'django.views.static.serve',
{'document_root': settings.MEDIA_ROOT, 'show_indexes': True}),
这个配置仍然可以在 Apache 下提供文件,但是不建议在生产中使用静态服务器。除了是一种非常低效的提供静态文件的方式之外,静态服务器代码还没有经过安全审计。因此,在生产中,这个 URL 模式应该从urls.py文件中删除,并且应该配置 Apache(或其他服务器)直接提供这些文件。
让 Apache 提供这些文件的指令是:
Alias /site_media /dj_projects/marketr/site_media
<Directory /dj_projects/marketr/site_media>
Order allow,deny
Allow from all
</Directory>
这些与为管理员媒体文件所需的指令几乎完全相同,只是修改为指定用于站点媒体文件的 URL 路径前缀和这些文件的实际位置。
这就是全部吗?还不是。与管理媒体文件不同,marketr项目使用的图像文件实际上是由marketr项目代码按需生成的。如果我们删除现有的图像文件并尝试访问已完成调查的详细页面,当 Web 服务器进程尝试创建其中一个图像文件时,我们将会收到错误,如下所示:
为了解决这个问题,Web 服务器代码需要对包含文件的目录具有写访问权限。这可以通过将目录/dj_projects/marketr/site_media/piecharts的所有者更改为www-data来实现,就像为 matplotlib 配置目录所做的那样。在我们进行了这个更改之后,尝试重新加载调查详细页面会显示 Web 服务器现在能够创建图像文件,如下所示:
我们现在已经在 Apache 下成功运行了项目。接下来,我们将考虑是否存在任何额外的潜在问题,这些问题可能是由于开发和生产 Web 服务器环境之间的差异而导致的。
测试多线程行为
在上一节中,我们遇到了在开发服务器和 Apache 下运行时的一些环境差异。其中一些(例如,文件权限和 Python 路径差异)导致了在我们能够使项目在 Apache 下正常运行之前必须克服的问题。我们观察到的一个差异是多线程,但我们还没有遇到与之相关的问题。
当我们在上一节中检查错误日志时,我们可以看到mod_wsgi已经启动了一个包含 15 个线程的进程,每个线程都准备处理一个传入的请求。因此,几乎同时到达服务器的多个请求将被分派到不同的线程进行处理,并且它们的执行步骤可能在实时中任意交错。这在开发服务器中永远不会发生,因为开发服务器是严格单线程的,确保每个请求在处理完之前不会启动下一个请求的处理。这也不会发生在前五章中介绍的任何测试工具中,因为它们也都以单线程方式进行测试。
在第九章中,我们已经注意到需要牢记潜在的多线程问题。在那一章中,我们编写了用于显示调查结果的图像文件生成代码。这些图像是在调查关闭后首次收到显示调查请求时按需生成的。生成图像并将其写入磁盘需要一定的时间,很明显,代码需要正确处理这样一种情况:当收到对调查结果的第二个请求时,第一个请求的处理尚未完成。
在那一章中,我们学习了如何在调试器中使用断点来强制多个线程按特定顺序执行。通过这种方式,我们看到了如何测试以确保代码在多线程环境中可能出现的最坏情况下的交错执行场景中的行为是否正常。
但我们不仅需要关注那些需要大量时间的操作,比如生成图像或写文件,还需要关注在多线程环境下的高请求负载下,即使通常很快的请求处理也可能被中断并与同时处理的其他请求的处理交错。在多处理器机器上,甚至不需要中断一个请求:第二个请求可能会在第二个处理器上真正同时运行。
在marketr项目中是否有任何代码可能在多线程环境下无法正常工作?可能有。通常,首先要考虑潜在的多线程问题的代码是更新数据的任何代码。对于survey应用程序,有一个视图在服务器上更新数据:接收和记录发布的调查结果的视图。
当我们在多线程环境中运行时,我们是否确定调查结果记录代码在运行时能够正常工作,其中可能会同时运行许多副本?由于我们还没有测试过,所以不能确定。但是现在我们已经在多线程环境中运行代码,我们可以尝试测试它并查看结果。
使用 siege 生成负载
在多线程环境中测试代码的可用性只是有效测试多线程行为所需的一半。另一半是一些方式来生成服务器处理的许多同时请求。有许多不同的工具可以用于此。我们将在这里使用的是称为siege的工具,这是由 Jeffrey Fulmer 编写的一个免费的命令行工具。有关下载和安装siege的信息可以在www.joedog.org/index/siege-home找到。
安装后,siege非常容易使用。调用它的最简单方法是在命令行上传递一个 URL。它将启动几个线程,并不断请求传递的 URL。在运行时,它会显示它正在做什么以及有关它正在接收的响应的关键信息。例如:
kmt@lbox:/dj_projects/marketr$ siege http://localhost:8080/
** SIEGE 2.66
** Preparing 15 concurrent users for battle.
The server is now under siege...
HTTP/1.1 200 0.06 secs: 986 bytes ==> /
HTTP/1.1 200 0.04 secs: 986 bytes ==> /
HTTP/1.1 200 0.04 secs: 986 bytes ==> /
HTTP/1.1 200 0.02 secs: 986 bytes ==> /
HTTP/1.1 200 0.03 secs: 986 bytes ==> /
HTTP/1.1 200 0.03 secs: 986 bytes ==> /
HTTP/1.1 200 0.03 secs: 986 bytes ==> /
HTTP/1.1 200 0.03 secs: 986 bytes ==> /
HTTP/1.1 200 0.04 secs: 986 bytes ==> /
在这里,我们看到调用siege来不断请求项目主页。在启动时,它报告了它的版本,并打印出它将使用多少线程来进行同时请求。默认值如此,为 15;-c(用于并发)命令行开关可以用来更改。然后,siege打印出有关它发送的每个请求的信息。对于每个请求,它打印出所使用的协议(这里都是HTTP/1.1),收到的响应代码(200),响应到达所花费的时间(在.02和.06秒之间),响应中的字节数(986),最后是请求的 URL 路径。
默认情况下,siege将一直运行,直到通过Ctrl-C中断。中断时,它将停止生成负载,并报告结果的统计信息。例如:
HTTP/1.1 200 0.11 secs: 986 bytes ==> /
HTTP/1.1 200 0.47 secs: 986 bytes ==> /
^C
Lifting the server siege... done.
Transactions: 719 hits
Availability: 100.00 %
Elapsed time: 35.02 secs
Data transferred: 0.68 MB
Response time: 0.21 secs
Transaction rate: 20.53 trans/sec
Throughput: 0.02 MB/sec
Concurrency: 4.24
Successful transactions: 719
Failed transactions: 0
Longest transaction: 0.79
Shortest transaction: 0.02
这个工具发出了略多于 700 个请求,所有请求都收到了响应,正如报告所示,可用性达到了 100%,没有失败的交易。报告的性能数字很有趣,但由于我们目前是在一个开发机器上运行,调试仍然打开,现在读取性能数字还为时过早。我们真正想要检查的是,在高负载的多线程环境下调用处理调查响应的代码是否正确。我们将考虑下一步该如何做。
测试结果记录代码的负载
我们如何使用siege来测试记录调查答案的代码?首先,我们需要在数据库中有一个仍然开放的调查,因此将接受发布的答复。最简单的方法是使用管理应用程序,并将现有的电视趋势调查的“关闭”日期更改为将来的某个时间。同时,我们可以将调查中所有答案的答复计数更改为 0,这将使我们能够轻松地判断我们使用siege生成的所有答复是否被正确处理。
接下来,我们需要确定要指定给siege的 URL,以便它可以为调查表单发布有效数据。最简单的方法是在浏览器中打开显示调查表单的页面,并检查 HTML 源代码,看看表单字段的名称和每个字段的有效值是什么。在这种情况下,当我们检索http://localhost:8080/1/时,显示表单的源 HTML 如下:
<form method="post" action=".">
<div>
What is your favorite type of TV show?
<ul>
<li><label for="id_answer_0"><input type="radio" id="id_answer_0" value="1" name="answer" /> Comedy</label></li>
<li><label for="id_answer_1"><input type="radio" id="id_answer_1" value="2" name="answer" /> Drama</label></li>
<li><label for="id_answer_2"><input type="radio" id="id_answer_2" value="3" name="answer" /> Reality</label></li>
</ul>
How many new shows will you try this Fall?
<ul>
<li><label for="id_1-answer_0"><input type="radio" id="id_1-answer_0" value="4" name="1-answer" /> Hardly any: I already watch too much TV!</label></li>
<li><label for="id_1-answer_1"><input type="radio" id="id_1-answer_1" value="5" name="1-answer" /> Maybe 3-5</label></li>
<li><label for="id_1-answer_2"><input type="radio" id="id_1-answer_2" value="6" name="1-answer" /> I'm a TV fiend, I'll try them all at least once!</label></li>
</ul>
<button type="submit">Submit</button>
</div>
</form>
表单有两个单选组输入,一个名为answer,一个名为1-answer。answer的有效选择是1、2和3。1-answer的有效选择是4、5和6。因此,我们希望指示siege向http://localhost:8080/1/发送answer为1到3之间的值,并且1-answer为4到6之间的值。任意选择两个问题的第一个选项的方法是将 URL 指定为"http://localhost:8080/1/ POST answer=1&1-answer=4"。请注意,由于其中包含空格和&,在命令行上传递此 URL 时需要使用引号。
为了获得可预测的生成请求数,我们可以指定-r命令行开关,指定测试重复的次数。如果我们保留默认的并发线程数为 15,并指定 5 次重复,在测试结束时,我们应该看到两个选择的答案每个都有 5*15,或 75 票。让我们试一试:
kmt@lbox:/dj_projects/marketr$ siege -r 5 "http://localhost:8080/1/ POST answer=1&1-answer=4"
** SIEGE 2.66
** Preparing 15 concurrent users for battle.
The server is now under siege...
HTTP/1.1 302 0.12 secs: 0 bytes ==> /1/
HTTP/1.1 302 0.19 secs: 0 bytes ==> /1/
HTTP/1.1 200 0.02 secs: 543 bytes ==> /thanks/1/
HTTP/1.1 302 0.15 secs: 0 bytes ==> /1/
HTTP/1.1 302 0.19 secs: 0 bytes ==> /1/
HTTP/1.1 302 0.37 secs: 0 bytes ==> /1/
HTTP/1.1 200 0.02 secs: 543 bytes ==> /thanks/1/
HTTP/1.1 302 0.30 secs: 0 bytes ==> /1/
这里的输出与第一个示例有些不同。survey应用程序对调查响应的成功 POST 是一个 HTTP 重定向(状态 302)。siege工具,就像浏览器一样,响应接收到的重定向,请求重定向响应中指定的位置。因此,先前的输出显示了 POST 请求成功,然后对调查感谢页面的后续重定向也成功。
此测试运行的输出末尾是:
HTTP/1.1 302 0.03 secs: 0 bytes ==> /1/
HTTP/1.1 200 0.02 secs: 543 bytes ==> /thanks/1/
HTTP/1.1 200 0.01 secs: 543 bytes ==> /thanks/1/
done.
Transactions: 150 hits
Availability: 100.00 %
Elapsed time: 9.04 secs
Data transferred: 0.04 MB
Response time: 0.11 secs
Transaction rate: 16.59 trans/sec
Throughput: 0.00 MB/sec
Concurrency: 1.85
Successful transactions: 150
Failed transactions: 0
Longest transaction: 0.56
Shortest transaction: 0.01
看起来不错。事务总数是请求的帖子数量的两倍,表明所有 POST 请求都返回了重定向,因此它们都被成功处理。因此,从客户端的角度来看,测试似乎运行成功。
但是服务器上的投票计数是否符合我们的预期?答案 1(喜剧)和 4(几乎没有:我已经看了太多电视了!)每个都被发布了 75 次,因此我们期望它们每个都有 75 票,而所有其他答案都没有。在管理应用程序中检查第一个问题的投票计数,我们看到以下内容:
类似地,检查第二个问题,我们看到以下内容:
这不好。虽然应该为 0 的votes值确实都是0,但是应该为 75 的两个votes值分别是40和34。根据发送给客户端的结果,服务器似乎成功处理了所有请求。然而,显然许多投票实际上并没有被记录。这是怎么发生的?答案在试图记录发布的调查响应的代码中,我们将在下面检查。
修复结果记录代码
请回想一下,记录发布的调查答案的代码位于survey/views.py中的display_active_survey函数中。此代码处理 GET 和 POST 请求。在 POST 的情况下,用于验证和记录提交值的代码是:
if request.method == 'POST':
chosen_answers = []
for qf in qforms:
if not qf.is_valid():
logging.debug("form failed validation: %r", qf.errors)
break;
chosen_answers.append(qf.cleaned_data['answer'])
else:
for answer in chosen_answers:
answer.votes += 1
answer.save(force_update=True)
return HttpResponseRedirect(reverse('survey_thanks', args=(survey.pk,)))
当单个线程依次运行时,此代码运行良好并且行为正常。但是,如果多个线程(来自相同或不同的进程)同时运行,都尝试增加相同答案的votes值,那么这段代码很可能会丢失投票。问题在于检索当前的votes值,增加它并保存新值不是原子操作。而是在可能与另一个线程同时交错进行的三个不同步的步骤中完成。
考虑两个并发运行的线程,都试图记录对主键值为 1 的Answer进行投票。(为简单起见,我们假设调查中只有一个问题。)第一个线程进入这段代码,并通过for qf in qforms循环验证表单。在这个循环中,将从数据库中读取所选答案的当前votes值。假设第一个线程读取的主键为 1 的答案的votes值为 5。
现在,在第一个线程能够完成其工作并将votes字段的递增值保存到数据库中之前,第二个线程(通过抢占式调度或多处理器执行)进入for qf in qforms循环。这第二个线程正在处理的发布的表单数据也指定了对主键为 1 的答案的投票。这第二个线程还读取了该答案的votes值的当前值为 5。现在我们有一个问题:两个线程都意图递增相同答案的votes值,都读取了相同的现有值,并且都将递增该值并保存结果。两个线程一起只会导致votes计数增加一次:一个投票实际上将会丢失。
我们如何解决这个问题?对于在数据库中对现有字段的值进行递增(或执行其他算术操作)的简单情况,可以相对容易地避免这个问题。我们可以稍微改变for answer in chosen_answers循环中的代码,使用 Django 的F表达式来描述votes的期望结果,而不是给它一个明确的数值。更改后的代码如下:
for answer in chosen_answers:
from django.db.models import F
answer.votes = F('votes') + 1
answer.save(force_update=True)
在votes的值中使用F表达式将导致 Django 构造一个UPDATE SQL 语句的形式:
UPDATE `survey_answer` SET `answer` = Comedy, `question_id` = 1, `votes` = `survey_answer`.`votes` + 1 WHERE `survey_answer`.`id` = 1
这种UPDATE语句将确保递增操作是原子的责任推到数据库服务器上。通常情况下,这就是您希望放置这种责任的地方,因为这正是数据库服务器应该正确和高效地执行的操作。
如果我们现在保存这个代码更改,将所有的投票计数重置为 0,并重新运行siege测试,问题应该就解决了。但事实并非如此!在运行测试后再次检查votes值显示相同的行为:对于应该具有 75 值的两个答案中,一个值为 43,另一个值为 39。为什么代码更改没有解决问题呢?
在这种情况下的问题是,代码更改没有被正在运行的 Web 服务器进程看到。在 Apache 下运行时,对 Django 应用程序代码的更改不会自动导致处理请求的进程重新加载。因此,现有的运行进程将继续使用旧代码。在守护程序模式下,触摸 WSGI 脚本将在接收到下一个请求时触发重新加载。或者,重新启动 Apache 将确保加载新代码。正如我们将在本章后面看到的,还可以编写 WSGI 脚本,以便在检测到代码更改时自动重新启动守护进程。
目前,由于现有的 WSGI 脚本不监视源代码更改,并且我们正在守护程序模式下运行,触摸 WSGI 脚本是加载应用程序代码更改的最简单方法。如果我们这样做,再次使用管理应用程序将投票计数重置为 0,并再次尝试siege测试,我们会发现当测试完成时,两个选择的答案的投票确实是正确的值,即 75。
额外的负载测试说明
虽然我们已经成功地发现并修复了接收和记录调查结果的代码中的多线程问题,但我们还没有进行足够的测试,以确保应用程序的其余部分在典型的生产环境中能够正常运行。完整的测试将涉及对所有视图进行负载测试,无论是单独还是与其他视图组合,并确保服务器正确响应。构建这样的测试超出了本书的范围,但这里包括了一些关于这个过程的注释。
首先,对于我们发现的问题,我们很幸运地发现一个非常简单的代码更改,即使用F表达式,可以轻松地使数据库更新具有原子性。对于其他情况,Django 可能会或可能不会提供一个简单的 API 来帮助确保更新的原子性。例如,对于创建对象,Django 确实有一个原子的get_or_create函数。对于更复杂的情况,比如涉及更新不同对象中的多个值的情况,可能没有一个简单的 Django API 可用于确保原子性。
在这些情况下,有必要使用数据库支持来维护数据一致性。一些数据库提供事务来帮助处理这个问题,Django 反过来提供了一个 API,允许应用程序控制事务行为。其他数据库不支持事务,但提供更低级别的支持,比如锁定表的能力。Django 不提供表锁定的 API,但它允许应用程序构建和执行任意(原始)SQL,因此应用程序仍然可以使用这样的功能。使用原始 SQL API 的缺点是,应用程序通常无法在不同的数据库上移植。
在创建新应用程序时,应仔细考虑应用程序需要执行的数据库更新类型。如果可能的话,最好构造数据,以便所有更新都可以使用简单的原子 API。如果不可能,那么可能需要使用数据库事务或更低级别的锁定支持。可用的选项范围可能会受到使用的数据库的限制(如果它是预定的),同样,用于确保数据一致性的特定技术的选择可能会限制应用程序最终能够正确运行的数据库。
其次,虽然仔细考虑和编码将有助于确保不会出现多线程意外,就像我们发现的一个 bug,但显式测试这类问题是一个好主意。不幸的是,这并不是第一至第五章涵盖的测试工具所支持的,这些工具都专注于验证正确的单线程行为。因此,通常需要一些额外的工作来增加单元测试套件,以确保在生产环境中的正确行为(可能还有一定的性能水平)。个别开发人员通常不太可能经常运行这些额外的测试,但是有这些测试可用,并在将任何代码更新放入生产环境之前运行它们,将会在长远节省麻烦。
在开发过程中使用 Apache/mod_wsgi
正如本章中所描述的,从使用 Django 开发服务器切换到像 Apache 与mod_wsgi这样的生产服务器可能会遇到各种各样的问题。有些问题很容易克服,其他可能需要更多的努力。通常在开发周期的后期遇到这些困难是不方便的,因为通常很少有时间进行代码更改。使过渡更加顺利的一种方法是在开发过程中使用生产服务器配置。这是一个值得认真考虑的想法。
使用生产服务器(即带有mod_wsgi的 Apache)在开发过程中可能会遇到的一个可能的反对意见是,安装和正确配置 Apache 很困难。要求个别开发人员这样做对他们来说要求过多。然而,安装通常并不困难,而且今天大多数开发机器都可以轻松运行 Apache 而不会对其他活动造成任何性能影响。
配置 Apache 确实可能令人生畏,因为有许多配置指令和可选模块需要考虑。然而,并不需要成为 Apache 配置专家才能成功地使用默认配置并修改它以支持运行 Django 应用程序。结果可能不会经过精细调整以在重载情况下获得良好的性能,但在开发测试期间并不需要对配置进行调整。
使用 Apache 在开发过程中的第二个反对意见可能是相对不便,与开发服务器相比。开发服务器的控制台提供了一种轻松查看正在进行的操作的方式;需要查看 Apache 日志文件有点麻烦。这是真的,但只是一个非常小的不便。
更严重的不便之处是需要确保运行的 Web 服务器进程在开发过程中重新启动以获取代码更改。很容易习惯于开发服务器的自动重启,并忘记需要做一些事情(即使只是简单地触摸 WSGI 脚本文件)来确保 Web 服务器使用最新的代码。
然而,实际上可以设置 Django 项目的 WSGI 脚本以与开发服务器相同的方式运行。也就是说,WSGI 脚本可以启动一个代码监视线程,检查更改的 Python 源文件,并在必要时触发自动重新加载。有关此内容的详细信息可以在code.google.com/p/modwsgi/wiki/ReloadingSourceCode找到。使用该页面上包含的代码,带有mod_wsgi配置的 Apache 几乎可以像 Django 开发服务器一样方便。
开发服务器的一个便利之处尚未涵盖的是能够轻松在代码中设置断点并进入 Python 调试器。即使在 Apache 下运行时也是可能的,但是为此 Apache 需要在控制台会话中以特殊模式启动,以便它有一个控制台允许调试器与用户交互。如何做到这一点的详细信息可以在code.google.com/p/modwsgi/wiki/DebuggingTechniques找到。
总之,几乎可以从 Apache/mod_wsgi设置中获得 Django 开发服务器的所有便利。在开发过程中使用这样的配置可以帮助轻松过渡到生产环境,并且值得在开发机器上安装和配置 Apache 与mod_wsgi的早期额外努力。
总结
我们现在已经讨论完将 Django 应用程序转移到生产环境的过程。在本章中,我们:
-
开发了一个配置来支持在 Apache 下使用
mod_wsgi运行marketr项目。 -
在将项目运行在 Apache 下遇到了一些问题。针对每个问题,我们都看到了如何诊断和解决。
-
考虑在新环境中可以进行哪些额外的测试,考虑到其能够同时运行多个线程。
-
为记录发布的调查响应的代码开发了一个测试,并观察到在生产环境下在重载情况下代码无法正确运行。
-
修复了在结果记录代码中发现的问题,并讨论了可能需要修复更复杂的多线程问题的其他技术。
-
讨论了在开发过程中使用 Apache 和
mod_wsgi的可能性。这种配置几乎可以和 Django 开发服务器一样方便,而在开发过程中使用生产环境设置可以帮助减少最终转移到生产环境时遇到的问题数量。