什么是PythonGIL,它是如何工作的,以及它如何影响gunicorn。我应该选择哪种Gunicornworker类型进行生产?Python有一个全局锁(GIL),它只允许一个线程运行(即解释字节码)。在我看来,如果您想优化Python服务,了解Python如何处理并发性是必不可少的。Python和gunicorn为您提供了处理并发的不同方法,并且由于没有涵盖所有用例的灵丹妙药,因此最好了解每种方法的选项、权衡和优势。Gunicorn工人类型Gunicorn在“工人类型”的概念下公开了这些不同的选项。每种类型都适用于一组特定的用例。sync-将进程分叉为N个并行运行的进程来处理请求。gthread-产生N个线程来同时处理请求。eventlet/gevent-生成绿色线程以同时处理请求。Gunicornsyncworker这是最简单的worker类型,其中唯一的并发选项是分叉N个进程,这些进程将并行处理请求。它们运行良好,但会产生大量开销(例如内存和CPU上下文切换),并且如果您的大部分请求时间都在等待I/O,则无法很好地扩展。Gunicorngthreadworker对此进行了改进,允许您为每个进程创建N个线程。这提高了I/O性能,因为您可以同时运行更多代码实例。这是受GIL影响的四个中的唯一一个。Gunicorneventlet和geventworkerseventlet/geventworkers试图通过运行轻量级用户线程(也称为绿色线程、greenlets等)进一步改进gthread模型。与系统线程相比,这允许您以一小部分成本拥有数千个所述的greenlet。另一个区别是它遵循协作工作模型而不是抢占式,允许不间断工作直到他们阻塞。我们将首先分析gthread工作线程在处理请求时的行为以及它如何受到GIL的影响。与每个请求直接由一个进程提供服务的同步不同,使用gthread每个进程都有N个线程可以更好地扩展,而不会产生多个进程的开销。由于您在同一个进程中运行多个线程,GIL将阻止它们并行运行。GIL不是进程或特殊线程。它只是一个布尔变量,其访问受互斥锁保护,以确保每个进程中只有一个线程在运行。它的工作方式可以在上图中看到。在此示例中,我们可以看到有2个系统线程并发运行,每个线程处理1个请求。流程是这样的:线程A持有GIL并开始为请求提供服务。一段时间后,线程B尝试为请求提供服务,但未能保持GIL。B设置超时以强制释放GIL,如果在达到超时之前没有发生这种情况。在达到超时之前,A不会释放GIL。B设置gil_drop_request标志强制A立即释放GIL。A释放GIL,会一直等到另一个线程获取到GIL,避免出现A不断释放抢占GIL,而其他线程抢不到的情况。B开始运行。B在阻塞I/O的同时释放GIL。A开始运行。B试图再次运行但被暂停。A在达到超时之前完成。B运行完成。相同的场景,但使用gevent在不使用进程的情况下增加并发性的另一种选择是使用greenlets。工作者产生“用户线程”而不是“系统线程”以增加并发性。虽然这意味着它们不受GIL的影响,但这也意味着您仍然无法增加并行度,因为它们无法由CPU并行调度。GreenletA将开始运行,直到发生I/O事件或执行完成。GreenletB将等待直到GreenletA释放事件循环。一个结束。B开始。B释放事件循环等待I/O。B完成了。对于这种情况,显然,拥有一个greenlet类型的工人并不理想。我们最终让第二个请求等到第一个请求完成,然后再次空闲等待I/O。在这些场景中,greenlet协作模型真正大放异彩,因为您不会将时间浪费在上下文切换上,并且避免了运行多个系统线程的开销。我们将在本文末尾的基准测试中见证这一点。现在,这引出了一个问题:更改线程上下文切换超时是否会影响服务延迟和吞吐量?当您混合I/O和CPU工作时如何在gevent/eventlet和gthread之间进行选择。如何使用gthreadworker选择线程数。我应该只使用同步工作者并增加分叉进程的数量来避免GIL吗?要回答这些问题,您需要进行监控以收集必要的指标,然后针对这些指标运行量身定制的基准测试。运行与您的实际使用模式零相关的综合基准测试是没有用的下图显示了不同场景的延迟和吞吐量指标,让您了解它们如何协同工作。对GIL切换间隔进行基准测试在这里,我们可以看到更改GIL线程切换间隔/超时如何影响请求延迟。正如预期的那样,随着切换间隔的减少,IO延迟会变得更好。发生这种情况是因为受CPU限制的线程被迫更频繁地释放GIL并允许其他线程完成它们的工作。但这不是万灵药。减少切换间隔将使受CPU限制的线程需要更长的时间才能完成。我们还可以看到整体延迟增加,超时减少,这是由于持续线程切换的开销增加所致。如果您想自己尝试,可以使用以下代码更改开关间隔:BenchmarkinggthreadvsgeventlatencywithCPUboundrequests分析工作产生的直觉。由于切换间隔强制释放长时间运行的线程,gthread对于IO绑定请求具有更好的平均延迟。geventCPU绑定请求比gthread具有更好的延迟,因为它们不会被中断以服务其他请求。BenchmarkinggthreadvsgeventthroughputusingCPUboundrequests这里的结果也反映了我们之前的直觉,即gevent的吞吐量优于gthread。这些基准高度依赖于正在完成的工作类型,不一定直接转化为您的用例。这些基准测试的主要目标是为您提供一些关于测试和测量内容的指导,以便最大限度地为请求提供服务的每个CPU内核。由于所有gunicornworker都允许您指定将运行的进程数,因此改变的是每个进程处理并发连接的方式。因此,请确保使用相同数量的工人以使测试公平。现在让我们尝试使用从我们的基准测试中收集的数据来回答前面的问题。更改线程上下文切换超时是否会影响服务延迟和吞吐量?的确。但是,对于绝大多数工作负载而言,它并不能改变游戏规则。当您混合I/O和CPU工作时,您如何在gevent/eventlet和gthread之间进行选择?正如我们所见,当您有更多CPU密集型工作时,ghtread倾向于允许更好的并发性。如何选择gthreadworker的线程数?只要您的基准测试模拟类似生产的行为,您就会清楚地看到峰值性能,然后它会因线程过多而开始下降。我应该只使用同步工作者并增加分叉进程的数量来避免GIL吗?除非您的I/O接近于零,否则仅使用进程进行扩展并不是最佳选择。结论Coroutines/Greenlets可以提高CPU效率,因为它们避免了线程间的中断和上下文切换。协程以延迟换取吞吐量。如果您混合使用IO和CPU绑定端点,协程可能会导致更不可预测的延迟-CPU绑定端点不会被中断以服务其他传入请求。如果您花时间正确配置gunicorn,GIL不是问题。
