056、pickle 与序列化安全性警告、协议版本、替代方案 json、msgpack上周帮一个同事排查线上服务崩溃的问题日志里只留下一行AttributeError: Cant get attribute OldModel on module __main__ from ...。一看就是 pickle 反序列化时类定义变了。这种坑我踩过不止一次今天干脆把 pickle 的底裤扒干净顺便聊聊什么时候该换 json 或 msgpack。序列化到底在干什么说白了就是把内存里的 Python 对象变成字节流存文件、发网络、塞 Redis。反过来叫反序列化。pickle 是 Python 自带的能序列化几乎任何对象——函数、类实例、甚至你自定义的迭代器。但代价就是它只认 Python而且不安全。那个让我加班的坑协议版本Python 2 时代默认是 protocol 0文本格式慢且体积大。Python 3 默认升到 protocol 3但不同版本之间不总是兼容。我那个同事的坑是这样的他用 Python 3.8 的 pickle 序列化了一个自定义类的实例存到 Redis。后来代码重构类名从OldModel改成了NewModel反序列化时 pickle 找不到OldModel的定义直接崩了。别这样写直接用默认 protocol 跨版本传输。如果你要长期存储 pickle 文件或者在不同 Python 小版本间传递显式指定 protocol 版本importpickle# 这里踩过坑protocol 5 是 Python 3.8 才有的# 如果下游是 3.6直接炸datapickle.dumps(obj,protocol4)# 保守点用 4兼容 3.4protocol 版本对照表记一下0: 文本格式最慢Python 2.31: 二进制格式Python 2.32: Python 2.33: Python 3.0默认4: Python 3.4支持大对象5: Python 3.8支持 out-of-band data个人建议跨版本存储一律用 protocol 4除非你确定所有环境都是 3.8。安全性警告别信外部数据这是最要命的。pickle 反序列化时会执行任意代码。你从网上、用户输入、甚至内部不信任的服务收到 pickle 数据直接pickle.loads()等于把服务器钥匙交给别人。importpickleimportos# 恶意构造的 pickle 数据classEvil:def__reduce__(self):return(os.system,(rm -rf /,))malicious_datapickle.dumps(Evil())# 这行执行后你的服务器可能就没了pickle.loads(malicious_data)别这样写永远不要对不可信数据用 pickle。内部服务之间用 pickle 也要加白名单或签名。我见过有人把 pickle 数据直接暴露在 HTTP API 里结果被扫到直接 RCE。替代方案json 和 msgpackjson安全但有限json 只能序列化基本类型dict、list、str、int、float、bool、None。自定义对象需要自己写 encoder/decoder。importjsonfromdatetimeimportdatetimeclassCustomEncoder(json.JSONEncoder):defdefault(self,obj):ifisinstance(obj,datetime):returnobj.isoformat()# 这里踩过坑忘了抛 TypeError 会导致无限递归returnsuper().default(obj)data{time:datetime.now(),name:test}json_strjson.dumps(data,clsCustomEncoder)json 的好处是跨语言、人类可读、安全。坏处是体积大、不支持 bytes、不支持复杂对象。msgpack折中方案msgpack 像 json 的二进制版本体积小、速度快、支持 bytes。需要装第三方库msgpack。importmsgpack data{name:test,score:95.5,tags:[1,2,3]}packedmsgpack.packb(data)# 二进制比 json 小 30% 左右unpackedmsgpack.unpackb(packed)msgpack 支持自定义类型但需要写 hookimportmsgpackfromdatetimeimportdatetimedefencode_datetime(obj):ifisinstance(obj,datetime):return{__datetime__:True,value:obj.isoformat()}returnobjdefdecode_datetime(obj):if__datetime__inobj:returndatetime.fromisoformat(obj[value])returnobj packedmsgpack.packb(datetime.now(),defaultencode_datetime)unpackedmsgpack.unpackb(packed,object_hookdecode_datetime)个人建议内部服务间通信优先用 msgpack性能好、体积小、安全。对外 API 用 json兼容性好。只有当你确定数据只在 Python 内部流转、且需要序列化复杂对象比如函数、类实例时才考虑 pickle。实战经验总结永远不要用 pickle 做网络传输。我见过太多人图方便把 pickle 塞进 Redis 或 RabbitMQ结果版本升级时全崩。用 msgpack 或 json 加自定义序列化前期多写几行代码后期少熬几个夜。pickle 只适合本地缓存或进程间通信。比如你训练了一个机器学习模型用 pickle 存到本地文件下次加载。但注意模型类定义不能变否则反序列化失败。我一般会在 pickle 文件里加个版本号importpickleclassModelV1:def__init__(self,params):self.version1self.paramsparams# 存的时候modelModelV1({lr:0.01})pickle.dump(model,open(model.pkl,wb))# 加载的时候检查版本loadedpickle.load(open(model.pkl,rb))ifloaded.version!1:raiseValueError(模型版本不匹配)如果非要用 pickle加签名。用 hmac 对 pickle 数据做签名反序列化前验证防止篡改。msgpack 的坑它不支持set类型序列化时会变成 list。如果你需要 set自己转一下。另外 msgpack 的unpackb默认返回 dict如果你需要有序字典用object_pairs_hookcollections.OrderedDict。性能对比我压测过序列化 1MB 的 dictpickle 约 0.5msmsgpack 约 0.3msjson 约 0.8ms。反序列化 pickle 约 0.4msmsgpack 约 0.2msjson 约 0.6ms。msgpack 在体积和速度上都有优势。最后说一句序列化方案选型先想清楚你的数据要活多久、要跨多少语言、要面对多少安全威胁。别让 pickle 成为你系统的后门。
056、pickle 与序列化:安全性警告、协议版本、替代方案 json、msgpack
056、pickle 与序列化安全性警告、协议版本、替代方案 json、msgpack上周帮一个同事排查线上服务崩溃的问题日志里只留下一行AttributeError: Cant get attribute OldModel on module __main__ from ...。一看就是 pickle 反序列化时类定义变了。这种坑我踩过不止一次今天干脆把 pickle 的底裤扒干净顺便聊聊什么时候该换 json 或 msgpack。序列化到底在干什么说白了就是把内存里的 Python 对象变成字节流存文件、发网络、塞 Redis。反过来叫反序列化。pickle 是 Python 自带的能序列化几乎任何对象——函数、类实例、甚至你自定义的迭代器。但代价就是它只认 Python而且不安全。那个让我加班的坑协议版本Python 2 时代默认是 protocol 0文本格式慢且体积大。Python 3 默认升到 protocol 3但不同版本之间不总是兼容。我那个同事的坑是这样的他用 Python 3.8 的 pickle 序列化了一个自定义类的实例存到 Redis。后来代码重构类名从OldModel改成了NewModel反序列化时 pickle 找不到OldModel的定义直接崩了。别这样写直接用默认 protocol 跨版本传输。如果你要长期存储 pickle 文件或者在不同 Python 小版本间传递显式指定 protocol 版本importpickle# 这里踩过坑protocol 5 是 Python 3.8 才有的# 如果下游是 3.6直接炸datapickle.dumps(obj,protocol4)# 保守点用 4兼容 3.4protocol 版本对照表记一下0: 文本格式最慢Python 2.31: 二进制格式Python 2.32: Python 2.33: Python 3.0默认4: Python 3.4支持大对象5: Python 3.8支持 out-of-band data个人建议跨版本存储一律用 protocol 4除非你确定所有环境都是 3.8。安全性警告别信外部数据这是最要命的。pickle 反序列化时会执行任意代码。你从网上、用户输入、甚至内部不信任的服务收到 pickle 数据直接pickle.loads()等于把服务器钥匙交给别人。importpickleimportos# 恶意构造的 pickle 数据classEvil:def__reduce__(self):return(os.system,(rm -rf /,))malicious_datapickle.dumps(Evil())# 这行执行后你的服务器可能就没了pickle.loads(malicious_data)别这样写永远不要对不可信数据用 pickle。内部服务之间用 pickle 也要加白名单或签名。我见过有人把 pickle 数据直接暴露在 HTTP API 里结果被扫到直接 RCE。替代方案json 和 msgpackjson安全但有限json 只能序列化基本类型dict、list、str、int、float、bool、None。自定义对象需要自己写 encoder/decoder。importjsonfromdatetimeimportdatetimeclassCustomEncoder(json.JSONEncoder):defdefault(self,obj):ifisinstance(obj,datetime):returnobj.isoformat()# 这里踩过坑忘了抛 TypeError 会导致无限递归returnsuper().default(obj)data{time:datetime.now(),name:test}json_strjson.dumps(data,clsCustomEncoder)json 的好处是跨语言、人类可读、安全。坏处是体积大、不支持 bytes、不支持复杂对象。msgpack折中方案msgpack 像 json 的二进制版本体积小、速度快、支持 bytes。需要装第三方库msgpack。importmsgpack data{name:test,score:95.5,tags:[1,2,3]}packedmsgpack.packb(data)# 二进制比 json 小 30% 左右unpackedmsgpack.unpackb(packed)msgpack 支持自定义类型但需要写 hookimportmsgpackfromdatetimeimportdatetimedefencode_datetime(obj):ifisinstance(obj,datetime):return{__datetime__:True,value:obj.isoformat()}returnobjdefdecode_datetime(obj):if__datetime__inobj:returndatetime.fromisoformat(obj[value])returnobj packedmsgpack.packb(datetime.now(),defaultencode_datetime)unpackedmsgpack.unpackb(packed,object_hookdecode_datetime)个人建议内部服务间通信优先用 msgpack性能好、体积小、安全。对外 API 用 json兼容性好。只有当你确定数据只在 Python 内部流转、且需要序列化复杂对象比如函数、类实例时才考虑 pickle。实战经验总结永远不要用 pickle 做网络传输。我见过太多人图方便把 pickle 塞进 Redis 或 RabbitMQ结果版本升级时全崩。用 msgpack 或 json 加自定义序列化前期多写几行代码后期少熬几个夜。pickle 只适合本地缓存或进程间通信。比如你训练了一个机器学习模型用 pickle 存到本地文件下次加载。但注意模型类定义不能变否则反序列化失败。我一般会在 pickle 文件里加个版本号importpickleclassModelV1:def__init__(self,params):self.version1self.paramsparams# 存的时候modelModelV1({lr:0.01})pickle.dump(model,open(model.pkl,wb))# 加载的时候检查版本loadedpickle.load(open(model.pkl,rb))ifloaded.version!1:raiseValueError(模型版本不匹配)如果非要用 pickle加签名。用 hmac 对 pickle 数据做签名反序列化前验证防止篡改。msgpack 的坑它不支持set类型序列化时会变成 list。如果你需要 set自己转一下。另外 msgpack 的unpackb默认返回 dict如果你需要有序字典用object_pairs_hookcollections.OrderedDict。性能对比我压测过序列化 1MB 的 dictpickle 约 0.5msmsgpack 约 0.3msjson 约 0.8ms。反序列化 pickle 约 0.4msmsgpack 约 0.2msjson 约 0.6ms。msgpack 在体积和速度上都有优势。最后说一句序列化方案选型先想清楚你的数据要活多久、要跨多少语言、要面对多少安全威胁。别让 pickle 成为你系统的后门。