枪与玫瑰

趣店容器进化史


简介

趣店的容器化进程经历过三个里程碑:docker、单集群脚本化管理、多集群平台化管理。为了兼顾日常业务的需求开发,每一个里程均是由小部分人主导推动,由点及面地进行推广,并通过在小范围的试错中寻找最适合趣店业务场景的容器化方案。容器化为趣店的服务隔离及服务器统一化管理提供了基础条件,并且通过容器化迁移为趣店每月节省至少10万元服务器费用。(由于迁移工作以PHP服务作为试点,因此本文中的案例亦是以PHP为主)

https://static001.geekbang.org/infoq/d5/d5271d784613011be20a25c3012777d6.png

趣店容器进化史快速预览图


Docker

作为容器化推进的第一阶段,此阶段由开发主导,推广开发及测试环境容器化使用,并进行小部分服务线上容器化试用。

Docker入门

容器化推进初期,此时我们内部对于容器较为了解的人员并不多,开发不知道应该如何使用容器,运维对于如何维护容器下的服务也没有经验,因此在这个阶段我们着重对全体开发人员及运维人员进行初级容器入门分享,分享主要包括以下几个方面:

  • Docker环境搭建

主要用于引导开发人员搭建本地Docker开发环境,进行初步的容器概念建模。

  • Docker命令解析

https://static001.geekbang.org/infoq/21/21989159796c45c3988f537289c105b6.jpeg?x-oss-process=image/resize,p_80/auto-orient,1

docker命令解析分享资料

该分享主要讲解Docker的常用指令、拆解容器的部署流程并简要介绍通过Swarm进行集群部署的方式。

  • Dockerfile最佳实践

参考《Best practices for writing Dockerfiles》,分享如何以更优雅的方式编写Dockerfile。

Docker编排

我们的部分开发人员尝试更深层次地应用容器化,例如基于docker-compose推广docker在本地开发环境落地。这一推广对于微服务一类单个项目依托于多个服务的开发环境部署提供了极大的便利,同时也在开发环境的使用中进一步深化大家对容器的理解。在这一阶段开发了简易的K8s编排脚本,对新上线的小服务尝试使用K8s部署服务。


单集群脚本化管理

考虑到容器化仍处于尝试阶段且需要进行定制化脚本开发,因此第二阶段仍是以开发作为主导。本阶段开始对主要服务的小流量环境进行容器化迁移,通过开发更完善的K8s编排脚本以优化服务的持续集成与部署。

容器化服务迁移

随着全员对容器认知水平的提高,在这一阶段我们的小部分开发开始尝试进行线上小流量环境的迁移,迁移过程也曾遇到一些问题。

  • CoreDNS负载异常导致部分请求错误

现象:在这一阶段的迁移过程中由于K8s的CoreDNS负载异常,我们已迁移服务曾出现短暂的不可用(因服务分区部署的关系我们及时将部署于K8s服务的服务流量摘除)
解决方案:容器化迁移是各方(运维、开发、K8s服务提供商)的磨合阶段,在这一阶段应提前准备及演练运行于K8s的服务异常情况下的流量切换方案。由于业务服务对K8s基础服务的强依赖关系,基础服务的监控、异常转移均需提前完善及演练。

镜像管理

镜像管理作为容器化迁移不可或缺的一部分,自建的镜像仓库能够更好的保障内部服务镜像的安全性(镜像可能包含服务源码),且部署于内网的镜像仓库能够极大提高部署速度。为简化镜像的管理与维护,我们在内网部署开源的Harbor服务管理内部镜像。

CI/CD

在这一阶段我们通过自研的脚本(集成编排文件生成、镜像构建、部署)及Jenkins实现服务的CI/CD。由于这一阶段的CI/CD流程仍是试验阶段并无十分完善,这里暂时不展开叙述,较为完善的流程可参考下一阶段迁移的CI/CD。

日志收集

  • 编排日志

编排日志目前我们没有特意收集,大部分情况下还是部署或者调度出现问题的时候由运维进入集群内通过Kubectl查看日志情况。

  • 容器日志

由于大部分服务的日志都是往指定目录输出,目前并没有很好的利用容器的标准输出作为容器内部服务日志输出的统一出口,所以容器日志当前仍处于待挖掘阶段。

  • 服务日志

    • Nginx
    • PHP

除去常规的Nginx access_log,我们在迁移过程中还需要重点关注Nginx error_log及PHP error_log,极少部分请求可能会因迁移过程中的操作不当而引发异常,此时可通过排查服务的错误日志及时发现并修复问题。

  • 业务日志

由于我们的业务日志输出并无统一规范,因此无法通过常规的容器标准输出采集日志,而是通过Volume的方式将Pod的输出日志挂载至节点主机目录,再通过节点主机的Filebeat + Kafka将日志统一收集至日志服务器。

监控

  • 宿主机资源监控(Master、Node)

主机的资源监控包括:CPU、内存、磁盘、网卡流量等等,尽可能详细地收集主机监控信息对于异常情况下的问题排查有着极大的帮助。

  • 基础组件监控(如:CoreDNS)

围绕于集群服务的各种基础组件:kube-apiserver、kube-controller-manager、kube-scheduler、kubelet、kube-proxy、CoreDNS等等,也需要纳入监控范围,避免因为单个基础组件的异常影响整个集群内部业务服务的稳定性。

MySQL InnoDB架构(8.0版本)核心要点

MySQL InnoDB架构(8.0版本)核心要点

索引数据结构

二叉树

  • 演示:https://www.cs.usfca.edu/~galles/visualization/BST.html
  • 缺点:对于自增序列将蜕变成链表

红黑树

  • 演示:https://www.cs.usfca.edu/~galles/visualization/RedBlack.html
  • 缺点:树高度不可控,大数据量查找效率不高

Hash表

  • 缺点:不支持范围查找

B-Tree

  • 特点:节点中的数据索引从左到右递增
  • 演示:https://www.cs.usfca.edu/~galles/visualization/BTree.html

B+Tree

  • 特点
    • 非叶子结点只存储索引,叶子节点包含所有索引字段
    • 相比B-Tree增加叶子结点间的指针连接,提高区间访问速度(可用于范围查找)
    • 叶子、非叶子结点有序,非叶子结点索引页常驻内存(高版本)减少磁盘I/O
  • 演示:https://www.cs.usfca.edu/~galles/visualization/BPlusTree.html
  • MyISAM与InnoDB数据存储差异
    • MyISAM:通过MYI查找索引获得数据所在磁盘文件地址,根据地址从MYD获取对应数据(非聚集索引)
    • InnoDB:主键索引数据与索引存放在同一文件(聚集索引),非主键索引:叶子结点存放的数据为主键

Buffer Pool

  • 定义:InnoDB数据缓存区,用于缓存数据提高访问性能,启动默认申请128M内存

  • 默认大小:16KB,数据存取的基本单位,数据、索引均以页的形式体现

三种链表

  1. Free链表(空闲链表)

    • 记录Buffer Pool空闲页块
    • 作用:每次数据读取如果Buffer Pool中没有所需数据则从磁盘读取数据所在页数据存入Buffer Pool,可存入的空闲页块从空闲链表中获取
  2. Flush链表(刷写链表)

    • 记录Buffer Pool中哪些页块是脏页
    • 作用:每次数据修改仅修改Buffer Pool中的数据,由定时任务从刷写链表中将脏页数据持久化到磁盘中(1s或10s)
  3. LRU链表(最近最少使用链表)

    • 链表分为两个区域:热数据区域(5/8)、冷数据区域(3/8)
    • 作用:每次查找数据后将数据块插入头节点,Buffer Pool满后释放尾节点对应页数据
    • 优化:冷数据区域控制块同一页数据两次访问时间大于1秒时将控制块转移到热数据区域(解决全表扫描等短时间连续访问相同页数据导致热数据频繁淘汰问题)

Log Buffer

Redo Log(InnoDB)

  • 控制参数:innodb_flush_log_at_trx_commit
    • 0:事务提交时,不立即对redo log进行持久化,由后台线程处理写盘操作
    • 1:事务提交时,立即对redo log进行持久化(默认)
    • 2:事务提交时,立即将redo log写入操作系统缓冲区,由操作系统处理写盘操作
  • update操作流程
    1. 更新Buffer Pool页数据
    2. 生成redo log对象
    3. commit后持久化redo log(顺序I/O,MySQL安装后默认创建2个48M log文件,减少磁盘寻址消耗)
    4. 如果redo log文件均已写满则触发check point,将flush链表所标记页数据写入磁盘
    5. 记录Change Buffer

Bin Log(MySQL)

  • 用途:主从同步时使用

Undo Log

  • 用途:事务回滚时使用,用于记录修改前数据

Change Buffer

  • 作用:用于解偶更新操作对索引更新的影响,临时记录更新操作用于后续索引更新
  • update操作流程
    1. 更新操作涉及索引更新时,如果非主键索引页不在Buffer Pool中则先将本次修改记录在Change Buffer中
    2. 在下一次数据查询用到索引页时,先检查Change Buffer中是否有对应索引页的修改,如有则进行修改后放入Buffer Pool中用于后续使用

Double Write Buffer

  • 作用:用于解决部分磁盘页数据部分写入失败问题,操作系统每页4KB,InnoDB每页16KB(操作系统需分4次才能完成磁盘写入操作)
  • 写入操作
    1. 写入双写缓冲区(写入成功后删除Redo Log)
    2. 写入表空间

随机森林算法实践

这是一份随机森林算法的 Python 实践代码,如果你还不知道随机森林算法是干什么用的可先参考 《机器学习算法-随机森林》

代码运行环境
Python 2.7

扩展包依赖

jieba==0.37
scikit-learn==0.17

随机森林算法实践

# -*- coding: utf-8 -*-

import os
import gc
import jieba
from sklearn.externals import joblib
from sklearn.ensemble import RandomForestClassifier
from sklearn.cross_validation import train_test_split
jieba.initialize()


class RandomForest(object):
    """ 随机森林算法实践 """
    def __init__(self, is_save=False):
        self.__is_save = is_save
        self.__clf = None
        self.__train_data_feature = None

        # 加载已保存的训练集
        clf = RandomForestTools.train_data_load()
        train_data_feature = RandomForestTools.feature_data_load()
        if clf and train_data_feature:
            self.__clf = clf
            self.__train_data_feature = train_data_feature

    def build_train_data(self, pre_train_data_list, result_list, train_size=0.9):
        """
        构建训练集
        :param pre_train_data_list: list 需要训练的数据
        :param result_list list 训练数据对应的结果
        :param train_size: float 0<train_size<=1 训练集占总数据的比例
        :return: object 训练集
        """
        # 数据预处理
        print('Start pre-treat data.')
        train_data_list = []
        train_data_feature = set()
        for pre_train_data in pre_train_data_list:
            # 分词
            train_data = self.word_segmentation(pre_train_data)
            train_data_list.append(train_data)
            # 提取分词特征
            for feature in train_data:
                train_data_feature.add(feature)

        # 数据预处理
        data = self.pre_treat_data(train_data_list, train_data_feature)

        # 将训练集随机分成数份,以便自校验训练集准确率
        print('Start split train and test data.')
        data_train, data_test, result_train, result_test = train_test_split(data, result_list, train_size=train_size)

        # 开始训练随机森林,n_jobs设为-1自动按内核数处理数据
        print('Start training random forest.')
        clf = RandomForestClassifier(n_jobs=-1)
        self.__clf = clf.fit(data_train, result_train)
        self.__train_data_feature = train_data_feature
        if self.__is_save:
            # 保存训练集,及各项数据的特征值
            print('Save training result.')
            RandomForestTools.train_data_save(self.__clf)
            RandomForestTools.feature_data_save(train_data_feature)
        print("Build train data finish and accuracy is:%.2f ." %
              (self.__clf.score(data_test, result_test)))

    @staticmethod
    def word_segmentation(train_data):
        """
        分词处理
        :param train_data string 带分词数据
        :return set 分词结果
        """
        word_segmentation_result = set()
        for word in jieba.lcut(train_data):
            word_segmentation_result.add(word)
        return word_segmentation_result

    @staticmethod
    def pre_treat_data(train_data_list, train_data_feature, is_gc_collect=False):
        """
        数据预处理,从已有数据中处理出最终训练集数据
        :param train_data_list: list/set 待训练集内容分词
        :param train_data_feature set 待训练集内容分词特征
        :param is_gc_collect: boolean 数据预处理完成后是否执行垃圾回收
        :return: list 经过预处理的待训练数据
        """
        # 为规避特征保存及取出的过程中顺序打乱而造成数据不对应的情况统一排序
        train_data_feature = sorted(train_data_feature)
        message_list = RandomForestTools.one_hot_encode_feature(train_data_list, train_data_feature)
        if is_gc_collect:
            print('Finish one hot encoder.')
            # 手动执行垃圾回收避免内存占用过高被系统强制kill
            print('Garbage collector: collected %d objects.' % gc.collect())
        return message_list

    def predict(self, predict_data):
        """
        预测输入数据是否为坏样本
        :param predict_data: string 待预测数据
        :return: 预测结果
        """
        predict_data = self.word_segmentation(predict_data)

        data_test = self.pre_treat_data([predict_data], self.__train_data_feature)
        result = self.__clf.predict(data_test)
        return result[0]


class RandomForestTools(object):
    """ 训练集数据操作类 """
    TRAIN_DATA_FILE_DIR = '/tmp/'
    TRAIN_DATA_FILE = 'train_data.pkl'
    FEATURE_DATA_FILE = 'feature_data.pkl'

    @staticmethod
    def train_data_save(clf):
        """
        保存训练集数据
        :param clf:训练集
        :return: boolean True为成功保存,False为保存失败
        """
        filename = RandomForestTools.TRAIN_DATA_FILE_DIR + RandomForestTools.TRAIN_DATA_FILE
        return RandomForestTools.save(filename, clf)

    @staticmethod
    def train_data_load():
        """
        加载已保存的训练集数据
        :return: object/False 训练集数据存在时返回训练集,不存在时返回False
        """
        filename = RandomForestTools.TRAIN_DATA_FILE_DIR + RandomForestTools.TRAIN_DATA_FILE
        if RandomForestTools.exists(filename):
            return RandomForestTools.load(filename)
        else:
            return False

    @staticmethod
    def feature_data_save(feature_data):
        """
        保存特征数据
        :param feature_data: set 训练集的对应的特征数据
        :return: boolean True为成功保存,False为保存失败
        """
        filename = RandomForestTools.TRAIN_DATA_FILE_DIR + RandomForestTools.TRAIN_DATA_FILE
        return RandomForestTools.save(filename, feature_data)

    @staticmethod
    def feature_data_load():
        """
        加载已保存的特征数据
        :return: object 特征数据
        :notice: 加载前应主动检测数据集是否存在,返回的特征值顺序可能会被打乱
        """
        filename = RandomForestTools.TRAIN_DATA_FILE_DIR + RandomForestTools.FEATURE_DATA_FILE
        if RandomForestTools.exists(filename):
            return RandomForestTools.load(filename)
        else:
            return False

    @staticmethod
    def save(filename, python_object):
        if not os.path.isdir(os.path.dirname(filename)):
            os.mkdir(os.path.dirname(filename))
        if joblib.dump(python_object, filename):
            return True
        else:
            return False

    @staticmethod
    def load(filename):
        return joblib.load(filename)

    @staticmethod
    def exists(filename):
        if os.path.isfile(filename):
            return True
        else:
            return False

    @staticmethod
    def one_hot_encode_feature(data_list, data_set):
        """
        根据特征在内容中是否出现将数据格式化成二维二进制数组
        :param data_list: list/set 待格式化数据
        :param data_set: set 特征统计
        :return: list 二维数组
        """
        x, y = 0, 0
        serialize_list = []
        for data in data_list:
            tmp_serialize_list = []
            for key in data_set:
                # 分词以list的方式判断是否存在特征是否存在
                if isinstance(data, list) or isinstance(data, set):
                    tmp_serialize_list.append(1 if key in data else 0)
                elif isinstance(data, basestring) or isinstance(data, int):
                    tmp_serialize_list.append(1 if key == data else 0)
                y += 1
            serialize_list.append(tmp_serialize_list)
            x += 1
        return serialize_list


class RandomForestException(Exception):
    pass

if __name__ == '__main__':
    pre_train_data = [
        u'我很开心',
        u'我非常开心',
        u'我其实很开心',
        u'我特别开心',
        u'我超级开心',
        u'我不开心',
        u'我一点都不开心',
        u'我很不开心',
        u'我非常不开心',
        u'我很久没那么不开心了',
    ]
    result_list = [
        u'开心',
        u'开心',
        u'开心',
        u'开心',
        u'开心',
        u'不开心',
        u'不开心',
        u'不开心',
        u'不开心',
        u'不开心',
    ]
    rf = RandomForest()
    rf.build_train_data(pre_train_data, result_list)
    print(rf.predict(
        u'你猜我开心吗?',
    ))

你也可以在 Github 上找到它。

web安全建议

以下是将 HTML 内容完整转换为 Markdown 的结果,已按照规范调整格式并确保结构正确:


一、开始前的一些建议

  • 不要相信任何用户输入或第三方数据来源,包括$_GET$_POST$_FILES)、$_COOKIE$_SERVER的部分参数等。
  • HTML、PHP、MYSQL等使用统一的UTF-8编码。
  • 数据库SQL构造时尽量使用’包裹参数。
  • 参数对比时正确使用 =====
  • 尽量选择白名单而非黑名单式过滤。

二、关于SQL注入

1.SQL注入的产生原因

SQL注入的产生在于外界的输入改变了原本定义的SQL语意,譬如:

原本定义的SQL语意:

$_GET['name'] = 'abc';
$name = $_GET['name'];
SELECT * FROM admin where user_name = '$name';

最终生成的SQL为:

SELECT * FROM admin where user_name = 'abc';

外界输入改变后的语意:

$_GET['name'] = 'abc\' or \'a\'=\'a';
$name = $_GET['name'];
SELECT * FROM admin where user_name = '$name';

最终生成的SQL为:

SELECT * FROM admin where user_name = 'abc' or 'a'='a';

此时SQL的语意多加了原定之外的 or 'a'='a',于是注入产生。

2.SQL注入的防御

(1)使用PDO或mysqli预编译处理SQL

使用预处理语句时,PHP请求MySQL将SQL进行预编译,然后再发送参数,因此无论参数是何内容MySQL均将参数当作普通的字符串(或整型、浮点型)处理。

例如上一例子最终生成的SQL为:

SELECT * FROM admin where user_name = 'abc\' or \'a\'=\'a';

此时无法发生SQL语意的改变,故能防止SQL注入。

注意:PDO兼容大部分数据库,但需要PHP版本5.0+;mysqli只支持MySQL数据库并且需要MySQL数据库版本4.1.13+(服务端版本5.0.7+)。

(2)无法使用预编译时的替代方案

使用 addslashes 而非 mysql_real_escape_string 作为临时安全过滤函数,对于参数类型为整型时使用 intval 将变量转换成整型。

特殊场景:在GBK字符集环境下,0xbf27 经过 addslashes 转义后得到 0xbf5c27,数据库将 0xbf5c 当作单字节字符而剩余的 27 则被当作 ' 解析,故而仍能造成SQL注入。

版本差异:旧版PHP中 mysql_real_escape_string 必须在数据库连接后使用;新版PHP虽不再强制要求,但会抛出警告。


三、关于XSS

1.XSS产生的原因

正如《白帽子讲web安全》中所说:XSS本质是“HTML注入”,用户的数据被当成了HTML代码的一部分来执行。例如:

NGINX源码阅读

NGINX源码阅读指南

一、前言

  • 源码版本:2018-10-02 nginx-1.15.5
  • 环境说明:主要描述Darwin环境下的流程,与Linux环境下类似,Win32环境下可能会减少部分流程
  • 运行模式
    • Darwin/Linux等*nix类系统使用多进程方式运行
    • Win32使用多线程方式运行
  • 命名规范ngx_开头的变量多为全局变量
  • 模块命名ngx_model_name.c多为处理nginx配置中相应模块的配置处理,ngx_model_name_core_module.c多为该模块的核心(通用)处理逻辑

二、NGINX架构

1. 进程工作模式

1.1 多进程模式

  • master进程

    • 接收外部信号发送给worker进程(如stop、restart、reload等)
    • 监控worker进程运行状态,worker异常退出后重新启动新的worker进程
    • 缓存管理
  • worker进程

    • 处理基本网络事件,如http、mail请求等

1.2 单进程模式

  • 调试情况下使用,直接使用单进程处理网络事件

1.3 核心配置参数

配置项 配置块 值类型 默认值 说明
daemon main flag 1 是否使用守护进程模式开启服务
master_process main flag 1 是否开启master管理进程,主要用于nginx开发调试
timer_resolution main time 0 控制gettimeofday()系统调用时机
pid main string logs/nginx.pid 主进程pid的存放路径
lock_file main string logs/nginx.lock 用于不支持原子操作的系统使用文件锁
worker_processes main unit string(auto) 1
debug_points main uint 0 监测内部错误时中止或停止进程
user main string nobody 设置worker进程运行的用户和用户组
worker_priority main int 0 设置worker进程的调度优先级
worker_cpu_affinity main umask string(auto)
worker_rlimit_nofile main uint 修改worker进程的最大文件描述符限制
worker_rlimit_core main uint 修改worker进程的核心文件最大限制
worker_shutdown_timeout main time 0 设置worker进程的结束等待时间
working_directory main string 设置工作目录
env main string TZ 设置需要的环境变量
load_module main string 用于加载动态模块

2. 启动阶段

2.1 master处理流程

  1. 初始化阶段

「旁门右道」CURL持久连接技巧

「旁门右道」CURL持久连接技巧

一、背景与挑战

在高频网络请求场景中,传统TCP连接存在显著性能瓶颈:

  • 每次请求需要三次握手建立连接
  • 增加连接超时风险(平均30s默认超时)
  • 服务端连接状态维护成本高
  • 网络资源重复消耗

通过连接复用技术可实现:

  • 减少TCP连接建立次数
  • 降低端到端延迟
  • 提升系统吞吐量
  • 增强服务可靠性

二、技术原理与实现

1. 协议层支持

协议版本 连接复用特性 多路复用支持
HTTP/1.1 Keep-Alive机制
HTTP/2 基于TCP流的连接复用 ✅(最大并发流数限制)

2. CURL连接复用机制(核心流程)

// 连接复用核心逻辑(curl/lib/url.c)
if (reuse = ConnectionExists(data, conn, ...)) {
    // 重用现有连接
    reuse_conn(conn, conn_temp);
    free(conn);
    conn = conn_temp;
} else {
    // 建立新连接
    Curl_conncache_add_conn(data->state.conn_cache, conn);
}

3. 连接管理策略

  • 连接清理:周期性清理失效连接(默认1秒间隔)
if(elapsed >= 1000L) {
    Curl_conncache_foreach(..., call_disconnect_if_dead);
}
  • 连接池控制
    • 单主机最大连接数限制
    • 全局连接数上限控制
    • LRU算法淘汰策略

三、PHP实现方案

1. 单例模式连接复用类

class Curl {
    private static $instance = null;
    private $ch = null;

    private function __construct() {
        $this->ch = curl_init();
    }

    public static function getInstance() {
        if(!self::$instance instanceof self){
            self::$instance = new self();
        }
        return self::$instance;
    }

    // 支持HTTP/2.0的GET请求
    public function get2($url, $timeout = 3, $params = [], $headers = []) {
        $url = $this->buildQuery($url, $params);
        $this->setGeneralOption($url, $timeout, $headers, CURL_HTTP_VERSION_2_0);
        return $this->execute();
    }

    // 连接复用核心配置
    protected function setGeneralOption($url, $timeout, $headers, $httpVersion = CURL_HTTP_VERSION_1_1) {
        curl_setopt($this->ch, CURLOPT_URL, $url);
        curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($this->ch, CURLOPT_TIMEOUT, $timeout);
        curl_setopt($this->ch, CURLOPT_HTTP_VERSION, $httpVersion);
        // ...其他配置
    }

    public function __destruct() {
        $this->close();
    }
}

2. 使用示例

// 复用HTTP/1.1连接
$curl = Curl::getInstance();
$curl->get("https://example.com/api1");
$curl->get("https://example.com/api2"); // 复用已有连接

// HTTP/2多路复用
$curl2 = Curl::getInstance();
$curl2->get2("https://example.com/api3");
$curl2->get2("https://example.com/api4"); // 复用TCP连接

四、进阶优化方案

1. 连接复用策略对比

方案 优势 劣势 适用场景
单例模式 简单易用 请求间隔离性差 单线程场景
连接池管理 资源隔离 实现复杂 高并发场景
协程复用 轻量级并发 需要协程框架 微服务架构

2. HTTP/2多路复用优化

  • 并发控制:设置CURLOPT_PIPEWAITING控制并发流数
  • 流优先级:通过CURLOPT_STREAM_WEIGHT配置流优先级
  • 连接维护:定期发送PING帧保持连接活跃

3. 跨请求连接保持

// 使用FastCGI进程常驻
class ConnectionPool {
    private static $pools = [];

    public static function getPool($host) {
        if (!isset(self::$pools[$host])) {
            self::$pools[$host] = new ConnectionManager();
        }
        return self::$pools[$host];
    }
}

// FPM环境下使用Unix Socket保持连接
$socket = stream_socket_client("unix:///tmp/curl.sock", $errno, $errstr);

五、性能优化建议

  1. 连接池配置