web server 多进程处理web请求遇到的一些问题

 

uwsgi 处理web 请求使用了多进程的模式,接收到web请求后可能由不同的进程去处理。

 

问题背景:

 一、前段时间写了个api,  这个api 是在django项目中的,并且使用的是nginx+uwsgi的方式提供服务的。在api  中 使用了django 的 get_or_create来保证数据表的唯一性。

           

XXX.objects.get_or_create(custom_column_id=custom_column_id, l_id=l_id)
 

 

 二、调用这个api 的客户端只有一个进程,并且使阻塞式的串行调用,只有在第一个接口返回后才进行下一次的请求。

 三、结果经过长期的运行发现,数据表中多了很多重复的数据。

   

custom_column_idl_id
12
12

 

问题分析: 先查查get_or_create的源代码,发现其不是线程安全的。也就是说有两个进程或线程在同时执行的get_or_create的时候,可能会都进行Create,这样就会产生两条相同的数据。如下django 源码:

def get_or_create(self, defaults=None, **kwargs):
        """
        Looks up an object with the given kwargs, creating one if necessary.
        Returns a tuple of (object, created), where created is a boolean
        specifying whether an object was created.
        """
        lookup, params = self._extract_model_params(defaults, **kwargs)
        # The get() needs to be targeted at the write database in order
        # to avoid potential transaction consistency problems.
        self._for_write = True
        try:
            return self.get(**lookup), False
        except self.model.DoesNotExist:
            return self._create_object_from_params(lookup, params)

 

也就是说如果uwsgi 的多进程在同时并发处理请求时可能会导致这种重复的数据产生。但是疑惑的是,客户端是串行请求的。在一个请求完成后在发送另一个请求。仔细研究代码发现发送请求使用python的requests 库,并且加了timeout 限制,如下官方解释:

 

You can tell Requests to stop waiting for a response after a given number of seconds with the timeout parameter:
requests.get('http://github.com', timeout=0.001)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
requests.exceptions.Timeout: HTTPConnectionPool(host='github.com', port=80): Request timed out. (timeout=0.001)

 

也就是在timeout范围内没有收到响应,客户端就会结束。虽然客户端停止了,但是服务端还没有结束,这时候再处理同样的请求就可能会并发的调用get_or_create方法,造成重复数据的问题。做一个实验来证明request 在timeout 后,客户端停止了。但是服务端依然在处理。如下图,客户端在timeout=3 s 后就自动返回,然而服务端依然在运行,知道uwsgi超时后自动重启。

 
图一、客户端超时返回错误
 
 图二、服务端使用循环输出来验证


 图三,服务端在超时后才退出

 

 

注:uwsgi 是使用进程池的方式处理http请求,也就是每次请求到来的请求可能是不同的进程去处理的。当然在uwsgi 中可以配置请求处理的超时时间,如果在超时时间内还没有处理完,主进程会把这个处理请求的进程重启。

 

解决思路: 

一,调整调用api 时设置的超时时间,保证接受请求的api进程处理完毕。超时时间必须大于uwsgi进程的超时时间(目前使用该方法,观察了一个月没有数据重复的情况)。

二,如果能在数据库层面保证数据的唯一性是最好的。那样才能保证数据的唯一性。

 

<think>我们使用多进程处理HTTP请求,每个客户端连接由一个子进程处理。 步骤: 1. 创建套接字,绑定端口并监听。 2. 循环接受客户端连接。 3. 当有客户端连接时,创建一个子进程处理该连接。 4. 在子进程中,接收HTTP请求,解析请求方法(GET或POST),然后根据请求方法进行相应的处理。 5. 构造HTTP响应并发送给客户端。 6. 关闭连接。 注意:在父进程中,我们需要回收子进程资源,避免僵尸进程。这里我们使用信号处理忽略SIGCHLD信号,由init进程回收。 我们只处理GET和POST请求,其他请求返回501错误。 对于GET请求,我们解析请求的路径(可能包含查询参数),这里我们简单处理,只返回一个固定的响应或者根据路径返回文件(但题目要求简单,我们可以先返回一个简单的字符串)。 对于POST请求,我们会读取请求体,然后同样返回一个简单的响应。 为了简单,我们不解析复杂的HTTP头部,只读取第一行获取请求方法和路径,对于POST请求,我们还需要读取Content-Length以确定请求体的长度。 示例代码将使用Python的socket模块和多进程模块(os.fork)。 注意:在子进程处理请求后,要关闭连接套接字,然后退出子进程。父进程关闭客户端套接字(因为子进程已经处理,父进程中不需要,且fork后父进程和子进程都有套接字描述符,所以父进程在fork后需要关闭客户端套接字)。 信号处理:在父进程中设置忽略SIGCHLD信号,这样当子进程结束时,内核会回收,不会产生僵尸进程。 我们实现一个简单的路由,当访问根路径("/")时,返回一个简单的HTML表单用于提交GET和POST请求,同时对于GET请求的查询参数和POST请求的表单数据进行简单回显。 由于是示例,我们不考虑安全性、性能以及复杂的HTTP协议特性。 代码结构如下: 1. 创建套接字,绑定,监听。 2. 设置信号处理(忽略SIGCHLD)。 3. 循环accept。 4. 对于每个连接,fork子进程: 子进程: 接收请求数据 解析请求行 根据请求方法处理: 如果是GET:提取查询参数(如果有),构造响应。 如果是POST:读取Content-Length,然后读取请求体,解析表单数据(这里我们简单按key=value格式,不考虑编码等),构造响应。 发送响应 关闭连接,退出子进程。 父进程:关闭客户端套接字,继续循环。 注意:我们只处理HTTP/1.1的请求,但为了简单,我们不考虑持久连接(Connection: close),每次处理完一个请求就关闭连接。 响应格式: HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 Connection: close <html>...</html> 如果请求方法不支持,则返回: HTTP/1.1 501 Not Implemented ... (其他头部) <html>Method not implemented</html> 我们设计两个简单的页面: 1. 根路径(GET请求)返回一个表单,表单可以输入用户名,有两个提交按钮:一个GET提交,一个POST提交。 2. 处理GET和POST请求的路径(我们统一用根路径处理,即GET请求带查询参数,POST请求带表单数据),然后返回一个页面显示提交的数据。 示例代码:</think>### 多进程 HTTP 服务器实现(支持 GET/POST) 下面是一个使用 Python 实现的多进程 HTTP 服务器,支持 GET 和 POST 请求处理: ```python import socket import os import signal from urllib.parse import parse_qs, unquote def handle_request(client_socket): """处理客户端请求""" data = client_socket.recv(4096).decode('utf-8') if not data: return headers = data.split('\r\n') method, path, _ = headers[0].split() # 解析请求参数 params = {} if method == 'GET': if '?' in path: path, query = path.split('?', 1) params = parse_qs(query) elif method == 'POST': content_length = next((int(h.split(': ')[1]) for h in headers if h.startswith('Content-Length')), 0) body = data.split('\r\n\r\n', 1)[1][:content_length] params = parse_qs(body) # 路由处理 if path == '/': response = handle_root(method, params) else: response = handle_404() client_socket.sendall(response.encode('utf-8')) client_socket.close() def handle_root(method, params): """处理根路径请求""" if method == 'GET': html = """ <html><body> <h1>多进程 HTTP 服务器</h1> <form method="POST"> <input type="text" name="message" placeholder="输入消息"> <button type="submit">POST 提交</button> </form> <p>GET 示例: <a href="/?name=Alice&age=30">/?name=Alice&age=30</a></p> """ if params: html += "<h2>GET 参数:</h2><ul>" for key, values in params.items(): for value in values: html += f"<li>{unquote(key)}: {unquote(value)}</li>" html += "</ul>" html += "</body></html>" elif method == 'POST': message = unquote(params.get('message', [''])[0]) html = f""" <html><body> <h1>POST 请求已接收</h1> <p>消息内容: {message}</p> <a href="/">返回首页</a> </body></html> """ else: return handle_501() response = f"HTTP/1.1 200 OK\r\n" response += "Content-Type: text/html; charset=utf-8\r\n" response += f"Content-Length: {len(html)}\r\n" response += "Connection: close\r\n\r\n" response += html return response def handle_404(): """404 页面""" html = "<html><body><h1>404 Not Found</h1></body></html>" response = f"HTTP/1.1 404 Not Found\r\n" response += "Content-Type: text/html; charset=utf-8\r\n" response += f"Content-Length: {len(html)}\r\n\r\n" response += html return response def handle_501(): """501 错误""" html = "<html><body><h1>501 Not Implemented</h1></body></html>" response = f"HTTP/1.1 501 Not Implemented\r\n" response += "Content-Type: text/html; charset=utf-8\r\n" response += f"Content-Length: {len(html)}\r\n\r\n" response += html return response def main(): server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server_socket.bind(('0.0.0.0', 8080)) server_socket.listen(5) print("服务器监听在 http://localhost:8080") # 防止僵尸进程 signal.signal(signal.SIGCHLD, signal.SIG_IGN) while True: client_socket, addr = server_socket.accept() print(f"接受来自 {addr} 的连接") pid = os.fork() if pid == 0: # 子进程 server_socket.close() handle_request(client_socket) os._exit(0) else: # 父进程 client_socket.close() if __name__ == '__main__': main() ``` ### 关键实现说明 1. **多进程架构** - 主进程监听端口,接受新连接 - 为每个客户端连接创建子进程处理请求 - 使用 `signal.SIG_IGN` 自动回收子进程资源[^1] 2. **HTTP 请求处理** - **GET 请求**:解析 URL 中的查询参数 - **POST 请求**:解析请求体中的表单数据 - 使用 `urllib.parse` 模块处理 URL 编码 3. **路由系统** - `/` 路径:返回交互式表单页面 - 其他路径:返回 404 响应 - 支持参数展示和表单提交 4. **响应格式** - 符合 HTTP/1.1 标准 - 包含必要的头部信息 - 支持 HTML 内容渲染 ### 使用示例 1. 启动服务器: ```bash python server.py ``` 2. 访问测试: - GET 请求示例:`http://localhost:8080/?name=Alice&age=30` - POST 请求:在首页表单输入消息并提交 ### 进程模型示意图 ``` 主进程 (监听端口) │ ├── 子进程1 (处理客户端A请求) │ ├── 子进程2 (处理客户端B请求) │ └── 子进程n (处理客户端N请求) ``` 多进程模型充分利用了多核 CPU 资源,每个子进程独立处理客户端请求,避免了单进程阻塞问题[^1]。当客户端连接关闭时,子进程自动退出,由操作系统回收资源。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值