Docker环境运行Celery任务时出现OperationalError的排查求助
Django+Celery+Docker环境下PostgreSQL连接失败问题排查
问题背景
通过Docker容器化部署Django、Celery、Redis和PostgreSQL,重写Product模型的save方法,当对象创建时触发Celery任务向指定用户发送邮件,但任务执行时出现PostgreSQL连接失败的OperationalError,提示connection is bad: No such file or directory。
错误追踪信息
celery | [2023-07-17 13:00:22,978: INFO/MainProcess] Task notifications.tasks.send_email_to_subs[f2b05676-2a9c-4624-af12-d62cd7f2c83e] received django_store_api | [17/Jul/2023 13:00:23] "POST /admin/store/product/18/change/ HTTP/1.1" 302 0 celery | [2023-07-17 13:00:23,285: ERROR/ForkPoolWorker-2] Task notifications.tasks.send_email_to_subs[f2b05676-2a9c-4624-af12-d62cd7f2c83e] raised unexpected: OperationalError('connection is bad: No such file or directory\n\tIs the server running locally and accepting connections on that socket?') celery | Traceback (most recent call last): celery | File "/usr/local/lib/python3.10/site-packages/django/db/backends/base/base.py", line 289, in ensure_connection celery | self.connect() celery | File "/usr/local/lib/python3.10/site-packages/django/utils/asyncio.py", line 26, in inner celery | return func(*args, **kwargs) celery | File "/usr/local/lib/python3.10/site-packages/django/db/backends/base/base.py", line 270, in connect celery | self.connection = self.get_new_connection(conn_params) celery | File "/usr/local/lib/python3.10/site-packages/django/utils/asyncio.py", line 26, in inner celery | return func(*args, **kwargs) celery | File "/usr/local/lib/python3.10/site-packages/django/db/backends/postgresql/base.py", line 275, in get_new_connection celery | connection = self.Database.connect(**conn_params) celery | File "/usr/local/lib/python3.10/site-packages/psycopg/connection.py", line 728, in connect celery | raise ex.with_traceback(None) celery | psycopg.OperationalError: connection is bad: No such file or directory celery | Is the server running locally and accepting connections on that socket? celery | celery | The above exception was the direct cause of the following exception: celery | celery | Traceback (most recent call last): celery | File "/usr/local/lib/python3.10/site-packages/celery/app/trace.py", line 477, in trace_task celery | R = retval = fun(*args, **kwargs) celery | File "/usr/local/lib/python3.10/site-packages/celery/app/trace.py", line 760, in __protected_call__ celery | return self.run(*args, **kwargs) celery | File "/store/notifications/tasks.py", line 20, in send_email_to_subs celery | for recipient in recipients: celery | File "/usr/local/lib/python3.10/site-packages/django/db/models/query.py", line 398, in __iter__ celery | self._fetch_all() celery | File "/usr/local/lib/python3.10/site-packages/django/db/models/query.py", line 1881, in _fetch_all celery | self._result_cache = list(self._iterable_class(self)) celery | File "/usr/local/lib/python3.10/site-packages/django/db/models/query.py", line 91, in __iter__ celery | results = compiler.execute_sql( celery | File "/usr/local/lib/python3.10/site-packages/django/db/models/sql/compiler.py", line 1560, in execute_sql celery | cursor = self.connection.cursor() celery | File "/usr/local/lib/python3.10/site-packages/django/utils/asyncio.py", line 26, in inner celery | return func(*args, **kwargs) celery | File "/usr/local/lib/python3.10/site-packages/django/db/backends/base/base.py", line 330, in cursor celery | return self._cursor() celery | File "/usr/local/lib/python3.10/site-packages/django/db/backends/base/base.py", line 306, in _cursor celery | self.ensure_connection() celery | File "/usr/local/lib/python3.10/site-packages/django/utils/asyncio.py", line 26, in inner celery | return func(*args, **kwargs) celery | File "/usr/local/lib/python3.10/site-packages/django/db/backends/base/base.py", line 288, in ensure_connection celery | with self.wrap_database_errors: celery | File "/usr/local/lib/python3.10/site-packages/django/db/utils.py", line 91, in __exit__ celery | raise dj_exc_value.with_traceback(traceback) from exc_value celery | File "/usr/local/lib/python3.10/site-packages/django/db/backends/base/base.py", line 289, in ensure_connection celery | self.connect() celery | File "/usr/local/lib/python3.10/site-packages/django/utils/asyncio.py", line 26, in inner celery | return func(*args, **kwargs) celery | File "/usr/local/lib/python3.10/site-packages/django/db/backends/base/base.py", line 270, in connect celery | self.connection = self.get_new_connection(conn_params) celery | File "/usr/local/lib/python3.10/site-packages/django/utils/asyncio.py", line 26, in inner celery | return func(*args, **kwargs) celery | File "/usr/local/lib/python3.10/site-packages/django/db/backends/postgresql/base.py", line 275, in get_new_connection celery | connection = self.Database.connect(**conn_params) celery | File "/usr/local/lib/python3.10/site-packages/psycopg/connection.py", line 728, in connect celery | raise ex.with_traceback(None) celery | django.db.utils.OperationalError: connection is bad: No such file or directory celery | Is the server running locally and accepting connections on that socket?
docker-compose配置
services: # Django web: build: . volumes: - .:/store - static_volume:/store/static - media_volume:/store/media ports: - 8000:8000 env_file: - web.env image: app:django container_name: django_store_api command: python manage.py runserver 0.0.0.0:8000 # Redis redis: image: redis:7.0.5-alpine container_name: redis # Celery Worker celery-worker: restart: always build: context: . entrypoint: celery command: -A celery_app.app worker --loglevel=info volumes: - .:/store container_name: celery depends_on: - db - redis - web links: - redis # Flower flower: build: context: . container_name: flower command: celery -A celery_app.app flower links: - redis depends_on: - redis - web - db ports: - '5555:5555' volumes: - .:/store # PostgreSQL db: image: postgres:14.1-alpine restart: always env_file: - web.env ports: - '5432:5432' volumes: - db:/var/lib/postgresql/data volumes: db: driver: local static_volume: media_volume:
核心代码片段
celery_app.py
import os from celery import Celery from django.conf import settings os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') app = Celery('core', broker_connection_retry_on_startup=True) app.config_from_object('django.conf:settings') app.conf.broker_url = settings.CELERY_BROKER_URL app.autodiscover_tasks()
core/init.py
from celery_app import app as celery_store __all__ = ('celery_store',)
settings.py中的Redis配置
CELERY_BROKER_URL = 'redis://redis:6379/0'
Product模型
class Product(models.Model): product_name = models.CharField(max_length=255, verbose_name='Товар') description = models.TextField(verbose_name='Описание') price = models.IntegerField(verbose_name='Цена') discount_percent = models.PositiveIntegerField(default=0, verbose_name='Процент скидки') in_stock = models.BooleanField(default=False, verbose_name='В наличии') amount = models.IntegerField(verbose_name='Количество') send_email_created = models.BooleanField(default=False) vendor = models.ForeignKey(Vendor, on_delete=models.CASCADE, verbose_name='Поставщик') category = models.ForeignKey(Category, on_delete=models.CASCADE, verbose_name='Категория') brand = models.ForeignKey(Brand, on_delete=models.CASCADE, verbose_name='Бренд') created = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания') updated = models.DateTimeField(auto_now=True, verbose_name='Дата обновления') class Meta: verbose_name = 'Товар' verbose_name_plural = 'Товары' ordering = ['product_name', ] def __str__(self): return f'{self.product_name} - {self.price} руб.' def save(self, *args, **kwargs): from notifications.tasks import send_email_to_subs self.product_name = self.product_name.capitalize() if self.send_email_created: send_email_to_subs.delay( discount_percent=self.discount_percent, price_with_discount=self.get_price_with_discount(), product_id=self.pk ) super(Product, self).save(*args, **kwargs) def get_price_with_discount(self) -> int: return int(self.price - self.price / 100 * self.discount_percent) if self.discount_percent > 0 else self.price
Celery任务代码
@shared_task def send_email_to_subs(discount_percent: int, product_id: int, price_with_discount: int) -> None: """ Send email message to people subscribed on the listing """ recipients = Customer.objects.filter(on_mail_listing=True) email_subject = 'New Product In Stock' email_body_template = 'email/new_product_notification.txt' email_messages = [] for recipient in recipients: user_name = recipient.user_name email_body = render_to_string(email_body_template, { 'user_name': user_name, 'id': product_id, 'discount': discount_percent, 'price_with_discount': price_with_discount }) email_message = ( email_subject, email_body, 'your-email@example.com', [recipient.email], ) email_messages.append(email_message) send_mass_mail(email_messages)
问题原因及解决方案
1. Celery容器未加载数据库环境变量
Celery Worker容器未配置env_file,导致无法获取PostgreSQL的连接参数。Django和PostgreSQL都使用web.env文件存储数据库配置,Celery作为Django的异步进程,必须加载相同的环境变量才能访问数据库。
修复:在docker-compose.yml的celery-worker和flower服务中添加env_file配置:
celery-worker: # ... 其他配置 env_file: - web.env flower: # ... 其他配置 env_file: - web.env
2. 数据库连接地址错误
检查web.env中的数据库主机配置,必须使用db(docker-compose中PostgreSQL服务的名称),而非localhost或127.0.0.1。Docker容器间通过服务名称通信,localhost仅指向容器自身,无法访问其他容器。
正确的数据库配置示例:
DB_HOST=db DB_PORT=5432 DB_NAME=your_db_name DB_USER=your_db_user DB_PASSWORD=your_db_password # 或者使用DATABASE_URL DATABASE_URL=postgres://your_db_user:your_db_password@db:5432/your_db_name
3. 验证数据库连接
进入Celery容器手动测试连接,确认配置生效:
- 进入容器:
docker exec -it celery bash - 启动Python shell:
python manage.py shell - 执行查询:
from store.models import Customer print(Customer.objects.count())
若报错,检查环境变量是否加载:
import os print(os.environ.get('DB_HOST'))
4. 处理数据库启动延迟
depends_on仅保证容器启动顺序,不确保PostgreSQL服务就绪。可通过以下方式优化:
- 保持
restart: always配置,让Celery自动重试连接 - 添加启动脚本,等待PostgreSQL就绪后再启动Celery
额外优化建议
模型save方法中延迟导入任务的方式可优化为文件顶部导入(需避免循环导入),或使用字符串路径调用任务:
# 在文件顶部导入任务 from notifications.tasks import send_email_to_subs class Product(models.Model): # ... 其他字段 def save(self, *args, **kwargs): self.product_name = self.product_name.capitalize() if self.send_email_created: send_email_to_subs.delay( discount_percent=self.discount_percent, price_with_discount=self.get_price_with_discount(), product_id=self.pk ) super().save(*args, **kwargs)
内容的提问来源于stack exchange,提问作者Mihail Bury
相关产品推荐
相关产品推荐

