标签 分布式系统 下的文章

不要侥幸,35 岁以上的程序员不好找工作, 这是一个既定事实

首先无论是什么渠道, 对于普通人来说 35+ 的程序员, 不好就业, 就是一个既定事实。 甚至都不一定与自己的工作经历、学历 有多大的关系。

甚至我知道很多 35+ 的老哥们, 经验丰富, 985 大学毕业, 依然不好找工作, 这个不是个例。

我们不过多探究为何 35+ 的程序员不好就业, 我们可能需要更多关注, 怎么在这种大背景下「绝地求生」

这些方向可以让 35+ 程序员依然抢手

“35 岁危机”并非绝对,大量 35 岁以上的程序员仍能保持职业竞争力,甚至更受青睐,核心在于是否具备“不可替代性”:


技术深度型:在某一细分领域(如底层架构、算法优化、安全攻防)有深耕,成为行业公认的技术专家。例如,专注于分布式系统设计、AI 大模型工程化的资深工程师,35 岁后反而因经验稀缺而抢手。


业务融合型:熟悉特定行业(如金融、医疗、制造业)的业务逻辑,能将技术与行业需求深度结合。例如,懂银行业务的支付系统架构师、懂医疗流程的医疗信息化专家,年龄增长带来的业务经验反而成为优势。


管理转型型:从技术岗转型为技术管理(如 CTO、技术总监、团队负责人),具备带团队、做决策、对接业务的能力。这类岗位更看重“经验沉淀”和“资源整合能力”,35-45 岁往往是黄金期。


技术管理型 - 有坑

首先看看「管理型」, 我感觉上面三个「绝地求生」方向, 管理方向, 反而是最不考虑的, 其实很简单, 现在大社会都是紧缩模式,只有出局的业务,没有新业务开展了。 那么这个时候, 就出现一个更加严重的问题, 「技术管理系」岗位, 一个萝卜一个坑, 甚至可以说, 你无论技术有多牛逼, 但是没有那个坑位, 可能永远都上不去。

甚至还有一个比较搞笑的现象,都是很多中小公司离开一线很久的技术 leader , 找不到坑位了, 再想着来投递技术岗, 技术上基本上生疏很久了, 基本上很难再就业。 这种人真不在少数。

深耕技术性 - 有利有弊

这个其实是一个非常好的方向, 但是这种人往往都是大头兵, 或者叫做高级工具人。 首先需要花非常多的时间和精力去做深耕技术, 要时刻保持最前沿的技术储备, 最充沛的精力, 最丰富的热情。然后要去干最累的活儿, 干最难的事儿, 但是不一定有好结果。 很简单, 这个业务线没了, 那也只能去找下一份工作。 而且大头兵, 很容易为业务背锅。

都是高级打工仔了, 做的好, 是应该的, 做的不好就得背锅。

而且还要想办法跟 AI 做差异性竞争。 很简单, 做了一个非常好的工作架构, 然后 AI 可以用非常低的成本做替代, 那就白干了。

上面说了那么多缺点, 这个方向就真的那么不堪吗?其实也不是, 只要努力, 肯吃苦, 至少下限还是很高的。 因为这个路子, 就跟上大学一样,你只要一直读书, 肯吃苦, 就能上到 博士 。 做深耕技术也是一样的, 只要肯努力, 耐得住寂寞, 一直死磕下去, 基本上在一个方向都能有几刷子的。 对于迷茫型和努力型同学,这个也是最佳直选。

所以有利有弊, 各位同学可自行斟酌。

业务融合型 - 性价比之王

技术的价值最终要落地到业务中,30 + 程序员若能将技术能力与具体行业的业务逻辑深度绑定,会比 “纯技术专家” 更难被替代 —— 因为年轻人可以快速学会技术,但吃透一个行业的业务规则(如金融风控逻辑、医疗流程规范、制造业供应链协同)往往需要 5 年以上的沉淀。

这个才是我真正想跟大家聊一聊的方向。

机-会

技术大厂,前端-后端-测试,全国均有机-会,感兴趣可以试试。待遇和稳定性都还不错~

精通技术的业务专家成长之路

“技术 + 业务” 复合岗,核心是 让技术能力成为 “解读业务、解决业务痛点” 的工具,而非终点。

这种转型的价值在于:业务逻辑的沉淀周期长(5-10 年),年轻人可快速学会技术,但难以短期吃透行业规则,这正是 30 + 程序员的经验红利。以下从 “有价值的业务方向”“业务理解训练方法”“避坑要点” 三个维度展开,附具体实操步骤:

一、值得深耕的“技术+业务”方向(附核心业务逻辑与技术结合点)

选择业务方向的关键标准:业务逻辑复杂(有门槛)、监管严格(需经验规避风险)、技术与业务深度绑定(技术优化能直接带来业务收益)。以下是几个高价值领域:

  1. 金融科技(银行/保险/证券)

核心业务逻辑:金融行业的本质是“风险定价+资金流转”,涉及复杂的监管规则(如央行反洗钱、银保监会合规要求)、用户分层(高净值客户vs大众客户)、业务流程(信贷审批、理赔核保、交易清算)。
技术结合点:


信贷领域:用AI模型优化风控(需理解“逾期率”“不良率”等业务指标,以及征信数据、行为数据如何影响授信);


交易领域:低延迟交易系统(需理解股票/期货的“撮合规则”“涨跌停限制”,技术优化直接影响交易成功率);


保险领域:智能核保系统(需理解“健康告知”“免责条款”等业务规则,技术需实现“用户输入→规则匹配→核保结论”的自动化)。

为什么值得做:金融监管政策每年更新(如2025年央行新规对“消费贷资金用途监控”的要求),技术方案必须跟着业务规则调整,经验越丰富越能快速响应,年轻人易因不懂合规踩坑。


  1. 医疗健康(医院信息化/互联网医疗)

核心业务逻辑:医疗行业的核心是“患者诊疗全流程”,涉及医院内部流程(挂号、分诊、问诊、检查、缴费、取药)、医保政策(医保目录、报销比例、异地结算规则)、医疗安全(病历隐私、药品溯源)。
技术结合点:


医院信息系统(HIS):需理解“门诊/住院流程”(如门诊的“医生开单→药房发药”环节,技术需对接收费系统、药品库存系统);


互联网医疗:在线问诊平台需符合《互联网诊疗管理办法》(如“首诊不能线上”“电子处方流转规则”),技术架构要支持“医患身份核验→问诊记录留存→处方合规性校验”;


医疗大数据:医疗影像AI辅助诊断(需理解“CT/MRI影像的临床意义”,技术模型训练需结合医生诊断逻辑,而非纯数据拟合)。

为什么值得做:医疗流程标准化程度低(不同医院流程差异大),且涉及生命安全,技术方案容错率极低,需要“技术+临床经验”双重积累,30+的耐心和细致更具优势。


  1. 智能制造(工业互联网/工厂数字化)

核心业务逻辑:制造业的核心是“生产效率提升+成本控制”,涉及生产流程(订单排产、物料采购、车间加工、质量检测、物流配送)、设备管理(设备故障率、OEE设备综合效率)、供应链协同(供应商交付周期、库存周转率)。

技术结合点:


工业物联网(IIoT):设备数据采集与分析(需理解“数控机床的主轴温度、转速与产品精度的关系”,技术需将数据转化为“设备维护预警”等业务动作);


MES系统(制造执行系统):生产排产优化(需理解“订单优先级、物料齐套率、设备产能”的制约关系,技术算法要平衡“交付时效”与“生产成本”);


质量追溯系统:需理解“产品不良品的产生环节”(如焊接工艺参数异常导致的缺陷),技术需实现“生产数据→不良原因”的反向追溯。

为什么值得做:制造业数字化转型依赖“懂生产的技术人”,纯技术人员易陷入“为数字化而数字化”(比如盲目上物联网设备却不会分析数据),而有车间经验的技术人员能精准定位痛点(如某环节停机1小时损失5万元,技术优化需优先解决)。


  1. 跨境电商(平台型/品牌型)

核心业务逻辑:跨境电商的核心是“跨区域供需匹配”,涉及海外市场规则(如亚马逊的A+页面规则、TikTok Shop的物流时效要求)、跨境链路(报关、清关、海外仓配送)、本地化运营(语言、支付习惯、合规要求,如欧盟增值税VAT)。

技术结合点:

选品系统:需理解“海外市场需求”(如东南亚雨季对雨具的需求波动),技术通过爬虫+数据分析预测“潜力商品”;
跨境ERP:需对接“多国物流商API”“海关报关系统”,技术需处理“汇率换算”“多语言订单”“合规申报”等业务细节;
本地化营销工具:如TikTok直播带货的“实时翻译+弹幕互动”功能,技术需结合“海外用户互动习惯”(如欧美用户更关注产品参数,东南亚用户更关注价格)。

为什么值得做:跨境业务涉及“多国家、多规则、多链路”,技术方案需灵活适配(比如某国突然调整进口关税,系统需快速支持税率更新),经验能减少试错成本,年轻人易因不了解海外规则导致系统“水土不服”。

二、训练“业务理解能力”的5个实操步骤(从0到1建立业务思维)

技术人员常陷入“只懂代码不懂业务”的误区,核心问题是:习惯用“技术实现”倒推“业务需求”,而非从“业务目标”推导“技术价值”。以下步骤帮你系统性建立业务思维:

步骤1:从“被动接需求”到“主动问目标”——搞懂“业务为什么需要这个功能”

具体做法:每次接需求时,多问3个问题:


    “这个功能要解决用户的什么痛点?”(如“用户反馈支付失败率高”,而非只接“开发新支付渠道”);
    “这个功能的业务指标是什么?”(如“支付成功率从90%提升到99%”,而非“完成开发即可”);
    “如果这个功能上线后不达预期,备选方案是什么?”(理解业务的优先级和容错空间)。


案例:若业务方提“开发一个优惠券系统”,技术人员不应直接设计表结构,而是先问:“发优惠券是为了拉新还是促活?目标是提升客单价10%还是复购率20%?预算多少?”——这些决定了系统是否需要支持“新用户专属券”“满减叠加规则”等细节。

步骤2:画“业务流程图”——用可视化方式梳理业务环节(比写代码更重要)

工具:Figma(画流程图)、Visio(复杂流程)、甚至手绘;
核心要素:每个流程节点包含“谁(角色)→做什么(动作)→输入/输出什么(信息)→遇到异常怎么办(分支)”;
案例:画“电商退款流程”时,需明确:

    角色:用户、客服、财务、仓库;
    动作:用户发起退款→客服审核(是否符合7天无理由)→财务确认退款金额→仓库确认是否收到退货→系统打款;
    异常分支:“用户已拆封商品”是否支持退款?“仓库未收到货但用户说已寄出”如何处理?


价值:流程图能帮你发现“技术设计的盲区”(如漏考虑“退款失败后重试机制”),也能让你在和业务方沟通时“用他们的语言对话”(而非只说“接口、数据库”)。

步骤3:“泡在业务场景里”——亲身体验业务,而非只听业务方描述

具体做法:


    若做电商:自己下单、退货、咨询客服,记录每个环节的体验(如“退款到账时间长”可能是技术链路太长);
    若做医疗系统:去医院门诊“蹲点”,看医生如何开单、护士如何分诊、患者如何缴费(你会发现“医生开单时频繁切换系统”是真实痛点,技术可做集成优化);
    若做金融:假扮客户打电话给银行客服,咨询“信用卡逾期如何处理”(理解业务方常说的“催收流程”实际是怎样的)。


关键:技术人员容易“坐在办公室想当然”,而业务的真相往往藏在一线操作中。比如某团队开发“外卖骑手App”时,程序员亲自骑了3天车,才发现“高峰期导航频繁卡顿”是比“界面美观”更重要的问题。

步骤4:建立“业务知识体系”——像学技术一样系统化学习业务

方法:


    行业基础术语库:整理业务常用词(如金融的“拨备率”“LPR”,医疗的“DRG/DIP”“电子病历互联互通”),每个词注明“定义+业务意义”(如“DRG”是“按疾病诊断分组付费”,影响医院的收费和成本控制);
    监管规则清单:收集行业相关政策(如跨境电商的《跨境电子商务零售进口商品清单》,金融的《个人信息保护法》对数据采集的要求),标注“哪些规则会影响技术方案”(如数据本地化存储要求决定服务器部署位置);
    业务指标公式:搞懂核心KPI的计算逻辑(如“电商GMV=流量×转化率×客单价”,“银行不良率=不良贷款余额/总贷款余额”),理解技术优化如何影响这些指标(如“页面加载速度提升1秒→转化率提升2%→GMV增加X万元”)。


工具:用Notion或Excel整理,定期更新(如政策变动时),避免“业务术语听不懂”的尴尬。

步骤5:输出“业务-技术关联报告”——证明你能“用技术解决业务问题”

核心动作:每完成一个项目,写一份“技术方案如何支撑业务目标”的报告,包含:


    业务背景:项目要解决什么业务痛点(如“工厂因排产不合理,订单交付延迟率达15%”);
    技术方案:用了什么技术(如APS高级排产算法),为什么选这个技术(对比其他方案,该算法在“多品种小批量”场景下更优);
    业务效果:技术上线后,业务指标有何变化(如“交付延迟率从15%降至5%,每月减少违约金100万元”);
    经验沉淀:如果再遇到类似业务问题,技术方案可复用哪些部分(如“排产算法可适配其他工厂的生产模式”)。


价值:这份报告不仅是你“业务+技术”能力的证明(跳槽时可作为案例),更能倒逼你在项目中主动思考“技术的业务价值”,而非只关注“代码写得漂不漂亮”。

三、转型避坑:这3个误区会让你“既不像技术,也不像业务”


误区1:放弃技术深度,单纯“转业务”
复合岗的核心是“技术为根,业务为翼”,而非变成纯业务岗。比如做金融科技,若不懂分布式系统,就无法设计高并发的交易系统;若不懂AI,就无法优化风控模型。保留技术深度,同时叠加业务理解,才是不可替代的关键。


误区2:只学“表面业务”,不懂“业务本质”
比如做电商,知道“优惠券能促单”是表面,理解“不同面额的优惠券对不同客群(新用户vs老用户)的转化差异”才是本质;做医疗,知道“电子病历要存数据”是表面,理解“病历数据如何支持医生诊断决策”才是本质。多问“为什么”,穿透业务动作看目标。


误区3:等待“别人教业务”,而非主动获取
业务方通常很忙,不会系统性教你业务知识。要主动“找信息”:看行业报告(艾瑞、易观)、读专业书籍(如《支付战争》懂支付业务,《精益生产》懂制造流程)、加行业社群(如医疗信息化的“HIT专家网”)、甚至考行业证书(如PMP学项目管理,CFA基础懂金融)。




——转载自:晴小篆

在这里插入图片描述

摘要

随着智能终端越来越多,应用早就不再只运行在一台设备上。手机、平板、智慧屏、手表之间的协作,已经成了很常见的需求。在这种背景下,多设备任务该怎么分、分到哪台设备执行,就成了开发中绕不开的问题。

在鸿蒙系统中,这个问题并不是靠开发者“手动指定设备”来解决的,而是通过 设备能力感知 + 分布式调度机制 来完成。开发者更多关心的是:
这个任务适合干什么,而不是非要在哪台设备干。

本文会结合鸿蒙系统的分布式能力,介绍多设备任务分配的整体思路,并通过可运行的 Demo 代码,把这个过程完整跑一遍,最后再结合几个真实场景,聊聊它在实际项目中该怎么用。

引言

如果放在以前,一个应用基本只跑在一台手机上,最多考虑前后台切换。但现在不一样了:

  • 手机在你手里
  • 平板在桌子上
  • 智慧屏在客厅
  • 手表戴在手上

用户希望的是:
设备不同,但体验是连着的。

鸿蒙系统的分布式能力,正是为这种场景设计的。它不是简单的“跨设备通信”,而是把 任务、数据、能力 都变成可以在多设备之间流动的资源。

而多设备任务分配,本质上就是一句话:

把合适的任务,交给合适的设备去做。

鸿蒙多设备任务分配的整体思路

先发现设备,再谈分配

在鸿蒙系统中,只要设备在同一个分布式网络里,系统就能自动发现它们。
开发者不需要自己维护“设备表”,也不用关心设备什么时候上线、下线。

系统会帮你感知这些信息:

  • 设备类型(手机、平板、智慧屏)
  • 基本性能情况
  • 是否可信
  • 当前是否可用

你只需要在合适的时机拿到设备列表即可。

任务一定要能拆

多设备任务分配的前提是:
你的业务本身是能拆开的。

比如:

  • 页面展示是一块
  • 数据采集是一块
  • 计算处理是一块

如果一个任务从头到尾全写死在一个 Ability 里,那基本就没法分配了。

系统负责“怎么选设备”

在鸿蒙里,真正“选哪台设备执行”的逻辑,大部分是系统完成的:

  • 当前设备忙不忙
  • 网络情况好不好
  • 设备能力是否匹配
  • 是否更适合本地执行

开发者更多是通过 Ability 启动方式、Service 类型、数据同步方式 来间接影响分配结果。

核心实现方式一:跨设备启动 Ability

适合什么场景

这种方式最常见,适合:

  • 页面展示
  • 功能模块整体迁移
  • 用户可感知的交互任务

比如:
手机负责控制,平板负责显示大屏内容。

Demo:在平板上启动远程 Ability

import distributedDeviceManager from '@ohos.distributedDeviceManager';
import featureAbility from '@ohos.ability.featureAbility';

const BUNDLE_NAME = 'com.example.distributeddemo';

let deviceManager = distributedDeviceManager.createDeviceManager(BUNDLE_NAME);

function startRemotePage() {
  let devices = deviceManager.getTrustedDeviceListSync();

  devices.forEach(device => {
    if (device.deviceType === 2) { // 假设 2 表示平板
      let want = {
        bundleName: BUNDLE_NAME,
        abilityName: 'RemotePageAbility',
        deviceId: device.deviceId
      };
      featureAbility.startAbility(want);
    }
  });
}

代码说明

  • createDeviceManager:创建设备管理器
  • getTrustedDeviceListSync:获取可信设备列表
  • deviceType:用于简单区分设备类型
  • startAbility:指定 deviceId 后,Ability 会在远端设备启动

整个过程不需要你关心远端设备的进程、生命周期,系统会处理。

核心实现方式二:分布式 Service 执行任务

适合什么场景

这种方式更适合:

  • 计算密集型任务
  • 后台处理
  • 不需要 UI 的逻辑

比如:
手机采集数据,交给性能更强的设备做分析。

Demo:连接远端计算 Service

import featureAbility from '@ohos.ability.featureAbility';

function connectRemoteService(remoteDeviceId: string) {
  let want = {
    bundleName: 'com.example.distributeddemo',
    abilityName: 'ComputeServiceAbility',
    deviceId: remoteDeviceId
  };

  featureAbility.connectAbility(want, {
    onConnect(elementName, remote) {
      console.log('远程 Service 已连接');
      remote.sendMessage({
        command: 'startCompute',
        data: [1, 2, 3, 4]
      });
    },
    onDisconnect() {
      console.log('远程 Service 已断开');
    }
  });
}

代码说明

  • Service 在远端设备运行
  • 本地通过 IPC 的方式和远端通信
  • 计算逻辑完全在远端执行
  • 本地只负责发请求、收结果

这种方式非常适合“重计算、轻交互”的任务。

典型应用场景分析与示例

场景一:手机 + 平板的学习展示系统

场景说明

  • 手机负责控制、翻页
  • 平板负责展示课件内容

实现思路

  • 手机发现平板
  • 在平板启动展示 Ability
  • 通过分布式数据同步当前页码
import distributedData from '@ohos.data.distributedData';

async function syncPage(page: number) {
  let kvManager = distributedData.createKVManager();
  let store = await kvManager.getKVStore('pageStore');
  await store.put('current_page', page);
}

平板端监听数据变化,自动刷新页面。

场景二:多设备健康数据分析

场景说明

  • 手表采集心率
  • 手机做基础处理
  • 平板做数据可视化

实现思路

  • 手表同步原始数据
  • 手机过滤、预处理
  • 平板负责展示图表

核心在于:
任务不是“复制”,而是“分工”。

场景三:家庭智慧屏协同控制

场景说明

  • 手机是遥控器
  • 智慧屏负责 UI 展示
  • 计算逻辑放在智慧屏

实现思路

  • 手机只负责发指令
  • 智慧屏 Service 处理业务逻辑
  • 结果同步回手机

这种模式下,手机压力很小,体验反而更流畅。

常见问题 QA

Q1:我能不能指定“一定要某台设备执行”?

不推荐。
鸿蒙的设计思想是 声明需求,而不是指定设备
你可以通过能力需求去“引导”,但不建议写死。

Q2:设备突然下线怎么办?

系统会通知连接断开,
你需要做的只有一件事:
支持本地降级执行或重试。

Q3:分布式任务一定比本地慢吗?

不一定。
当任务本身就不适合本地执行时,
分布式反而更快、更省电。

总结

在鸿蒙系统中,多设备任务分配并不是一套复杂、难以理解的机制,它的核心思想其实很简单:

  • 把任务拆清楚
  • 描述好任务需求
  • 把调度交给系统

只要你在设计阶段考虑好“哪些任务适合分出去”,鸿蒙的分布式能力就能自然地帮你把事情做好。

一句话总结就是:

多设备任务分配,不是设备协作有多复杂,而是你有没有把任务设计清楚。

在这里插入图片描述

摘要

随着智能终端越来越多,应用早就不再只运行在一台设备上。手机、平板、智慧屏、手表之间的协作,已经成了很常见的需求。在这种背景下,多设备任务该怎么分、分到哪台设备执行,就成了开发中绕不开的问题。

在鸿蒙系统中,这个问题并不是靠开发者“手动指定设备”来解决的,而是通过 设备能力感知 + 分布式调度机制 来完成。开发者更多关心的是:
这个任务适合干什么,而不是非要在哪台设备干。

本文会结合鸿蒙系统的分布式能力,介绍多设备任务分配的整体思路,并通过可运行的 Demo 代码,把这个过程完整跑一遍,最后再结合几个真实场景,聊聊它在实际项目中该怎么用。

引言

如果放在以前,一个应用基本只跑在一台手机上,最多考虑前后台切换。但现在不一样了:

  • 手机在你手里
  • 平板在桌子上
  • 智慧屏在客厅
  • 手表戴在手上

用户希望的是:
设备不同,但体验是连着的。

鸿蒙系统的分布式能力,正是为这种场景设计的。它不是简单的“跨设备通信”,而是把 任务、数据、能力 都变成可以在多设备之间流动的资源。

而多设备任务分配,本质上就是一句话:

把合适的任务,交给合适的设备去做。

鸿蒙多设备任务分配的整体思路

先发现设备,再谈分配

在鸿蒙系统中,只要设备在同一个分布式网络里,系统就能自动发现它们。
开发者不需要自己维护“设备表”,也不用关心设备什么时候上线、下线。

系统会帮你感知这些信息:

  • 设备类型(手机、平板、智慧屏)
  • 基本性能情况
  • 是否可信
  • 当前是否可用

你只需要在合适的时机拿到设备列表即可。

任务一定要能拆

多设备任务分配的前提是:
你的业务本身是能拆开的。

比如:

  • 页面展示是一块
  • 数据采集是一块
  • 计算处理是一块

如果一个任务从头到尾全写死在一个 Ability 里,那基本就没法分配了。

系统负责“怎么选设备”

在鸿蒙里,真正“选哪台设备执行”的逻辑,大部分是系统完成的:

  • 当前设备忙不忙
  • 网络情况好不好
  • 设备能力是否匹配
  • 是否更适合本地执行

开发者更多是通过 Ability 启动方式、Service 类型、数据同步方式 来间接影响分配结果。

核心实现方式一:跨设备启动 Ability

适合什么场景

这种方式最常见,适合:

  • 页面展示
  • 功能模块整体迁移
  • 用户可感知的交互任务

比如:
手机负责控制,平板负责显示大屏内容。

Demo:在平板上启动远程 Ability

import distributedDeviceManager from '@ohos.distributedDeviceManager';
import featureAbility from '@ohos.ability.featureAbility';

const BUNDLE_NAME = 'com.example.distributeddemo';

let deviceManager = distributedDeviceManager.createDeviceManager(BUNDLE_NAME);

function startRemotePage() {
  let devices = deviceManager.getTrustedDeviceListSync();

  devices.forEach(device => {
    if (device.deviceType === 2) { // 假设 2 表示平板
      let want = {
        bundleName: BUNDLE_NAME,
        abilityName: 'RemotePageAbility',
        deviceId: device.deviceId
      };
      featureAbility.startAbility(want);
    }
  });
}

代码说明

  • createDeviceManager:创建设备管理器
  • getTrustedDeviceListSync:获取可信设备列表
  • deviceType:用于简单区分设备类型
  • startAbility:指定 deviceId 后,Ability 会在远端设备启动

整个过程不需要你关心远端设备的进程、生命周期,系统会处理。

核心实现方式二:分布式 Service 执行任务

适合什么场景

这种方式更适合:

  • 计算密集型任务
  • 后台处理
  • 不需要 UI 的逻辑

比如:
手机采集数据,交给性能更强的设备做分析。

Demo:连接远端计算 Service

import featureAbility from '@ohos.ability.featureAbility';

function connectRemoteService(remoteDeviceId: string) {
  let want = {
    bundleName: 'com.example.distributeddemo',
    abilityName: 'ComputeServiceAbility',
    deviceId: remoteDeviceId
  };

  featureAbility.connectAbility(want, {
    onConnect(elementName, remote) {
      console.log('远程 Service 已连接');
      remote.sendMessage({
        command: 'startCompute',
        data: [1, 2, 3, 4]
      });
    },
    onDisconnect() {
      console.log('远程 Service 已断开');
    }
  });
}

代码说明

  • Service 在远端设备运行
  • 本地通过 IPC 的方式和远端通信
  • 计算逻辑完全在远端执行
  • 本地只负责发请求、收结果

这种方式非常适合“重计算、轻交互”的任务。

典型应用场景分析与示例

场景一:手机 + 平板的学习展示系统

场景说明

  • 手机负责控制、翻页
  • 平板负责展示课件内容

实现思路

  • 手机发现平板
  • 在平板启动展示 Ability
  • 通过分布式数据同步当前页码
import distributedData from '@ohos.data.distributedData';

async function syncPage(page: number) {
  let kvManager = distributedData.createKVManager();
  let store = await kvManager.getKVStore('pageStore');
  await store.put('current_page', page);
}

平板端监听数据变化,自动刷新页面。

场景二:多设备健康数据分析

场景说明

  • 手表采集心率
  • 手机做基础处理
  • 平板做数据可视化

实现思路

  • 手表同步原始数据
  • 手机过滤、预处理
  • 平板负责展示图表

核心在于:
任务不是“复制”,而是“分工”。

场景三:家庭智慧屏协同控制

场景说明

  • 手机是遥控器
  • 智慧屏负责 UI 展示
  • 计算逻辑放在智慧屏

实现思路

  • 手机只负责发指令
  • 智慧屏 Service 处理业务逻辑
  • 结果同步回手机

这种模式下,手机压力很小,体验反而更流畅。

常见问题 QA

Q1:我能不能指定“一定要某台设备执行”?

不推荐。
鸿蒙的设计思想是 声明需求,而不是指定设备
你可以通过能力需求去“引导”,但不建议写死。

Q2:设备突然下线怎么办?

系统会通知连接断开,
你需要做的只有一件事:
支持本地降级执行或重试。

Q3:分布式任务一定比本地慢吗?

不一定。
当任务本身就不适合本地执行时,
分布式反而更快、更省电。

总结

在鸿蒙系统中,多设备任务分配并不是一套复杂、难以理解的机制,它的核心思想其实很简单:

  • 把任务拆清楚
  • 描述好任务需求
  • 把调度交给系统

只要你在设计阶段考虑好“哪些任务适合分出去”,鸿蒙的分布式能力就能自然地帮你把事情做好。

一句话总结就是:

多设备任务分配,不是设备协作有多复杂,而是你有没有把任务设计清楚。

今天跟大家分享一个etcd的内存大量占用的问题,这是前段时间在我们开源软件Easegress中遇到的问题,问题是比较简单的,但是我还想把前因后果说一下,包括,为什么要用etcd,使用etcd的用户场景,包括etcd的一些导致内存占用比较大的设计,以及最后一些建议。希望这篇文章不仅仅只是让你看到了一个简单的内存问题,还能让你有更多的收获。当然,也欢迎您关注我们的开源软件,给我们一些鼓励。

为什么要用ETCD

先说一下为什么要用etcd。先从一个我们自己做的一个API网关 – Easegress(源码)说起。

Easegress 是我们开发并开源的一个API应用网关产品,这个API应用网关不仅仅只是像nginx那样用来做一个反向代理,这个网关可以做的事很多,比如:API编排、服务发现、弹力设计(熔断、限流、重试等)、认证鉴权(JWT,OAuth2,HMAC等)、同样支持各种Cloud Native的架构如:微服务架构,Service Mesh,Serverless/FaaS的集成,并可以用于扛高并发、灰度发布、全链路压力测试、物联网……等更为高级的企业级的解决方案。所以,为了达到这些目标,在2017年的时候,我们觉得在现有的网关如Nginx上是无法演进出来这样的软件的,必需重新写一个(后来其他人也应该跟我们的想法一样,所以,Lyft写了一个Envoy。只不过,Envoy是用C++写的,而我用了技术门槛更低的Go语言)

另外,Easegress最核心的设计主要有三个:

  • 一是无第三方依赖的自己选主组集群的能力
  • 二是像Linux管道命令行那样pipeline式的插件流式处理(支持Go/WebAssembly)
  • 三是内置一个Data Store用于集群控制和数据共享。

对于任何一个分布式系统,都需要有一个强一制性的基于Paxos/Raft的可以自动选主机制,并且需要在整个集群间同步一些关键的控制/配置和相关的共享数据,以保证整个集群的行为是统一一致的。如果没有这么一个东西的话,就没有办法玩分布式系统的。这就是为什么会有像Zookeeper/etcd这样的组件出现并流行的原因。注意,Zookeeper他们主要不是给你存数据的,而是给你组集群的。

Zookeeper是一个很流行的开源软件,也被用于各大公司的生产线,包括一些开源软件,比如:Kafka。但是,这会让其它软件有一个依赖,并且在运维上带来很大的复杂度。所以,Kafka在最新的版本也通过内置了选主的算法,而抛弃了外挂zookeeper的设计。Etcd是Go语言社区这边的主力,也是kubernetes组建集群的关键组件。Easegress在一开始(5年前)使用了gossip协议同步状态(当时想的过于超前,想做广域网的集群),但是后发现这个协议太过于复杂,而且很难调试,而广域网的API Gateway也没遇到相应的场景。所以,在3年前的时候,为了稳定性的考量,我们把其换成了内嵌版本的etcd,这个设计一直沿用到今天。

Easegress会把所有的配置信息都放到etcd里,还包括一些统计监控数据,以及一些用户的自定义数据(这样用户自己的plugin不但可以在一条pipeline内,还可以在整个集群内共享数据),这对于用户进行扩展来说是非常方便的。软件代码的扩展性一直是我们追求的首要目标,尤其是开源软件更要想方设法降低技术门槛让技术易扩展,这就是为什么Google的很多开源软件都会选使用Go语言的原因,也是为什么Go正在取代C/C++的做PaaS基础组件的原因。

背景问题

好了,在介绍完为什么要用etcd以后,我开始分享一个实际的问题了。我们有个用户在使用 Easegress 的时候,在Easegress内配置了上千条pipeline,导致 Easegress的内存飙升的非常厉害- 10+GB 以上,而且长时间还下不来。

用户报告的问题是——

在Easegress 1.4.1 上创建一个HTTP对象,1000个Pipeline,在Easegres初始化启动完成时的内存占用大概为400M,运行80分钟后2GB,运行200分钟后达到了4GB,这期间什么也没有干,对Easegress没有进行过一次请求。

一般来说,就算是API再多也不应该配置这么多的处理管道pipeline的,通常我们会使用HTTP API的前缀把一组属于一个类别的API配置在一个管道内是比较合理的,就像nginx下的location的配置,一般来说不会太多的。但是,在用户的这个场景下配置了上千个pipeline,我们也是头一次见,应该是用户想做更细粒度的控制。

经过调查后,我们发现内存使用基本全部来自etcd,我们实在没有想到,因为我们往etcd里放的数据也没有多少个key,感觉不会超过10M,但不知道为什么会占用了10GB的内存。这种时候,一般会怀疑etcd有内存泄漏,上etcd上的github上搜了一下,发现etcd在3.2和3.3的版本上都有内存泄露的问题,但都修改了,而 Easegress 使用的是3.5的最新版本,另外,一般来说内存泄漏的问题不会是这么大的,我们开始怀疑是我们哪里误用了etcd。要知道是否误用了etcd,那么只有一条路了,沉下心来,把etcd的设计好好地看一遍。

大概花了两天左右的时间看了一下etcd的设计,我发现了etcd有下面这些消耗内存的设计,老实说,还是非常昂贵的,这里分享出来,避免后面的同学再次掉坑。

首当其冲是——RaftLog。etcd用Raft Log,主要是用于帮助follower同步数据,这个log的底层实现不是文件,而是内存。所以,而且还至少要保留 5000 条最新的请求。如果key的size很大,这 5000条就会产生大量的内存开销。比如,不断更新一个 1M的key,哪怕是同一个key,这 5000 条Log就是 5000MB = 5GB 的内存开销。这个问题在etcd的issue列表中也有人提到过  issue #12548 ,不过,这个问题不了了之了。这个5000还是一个hardcode,无法改。(参看 DefaultSnapshotCatchUpEntries 相关源码

// DefaultSnapshotCatchUpEntries is the number of entries for a slow follower
// to catch-up after compacting the raft storage entries.
// We expect the follower has a millisecond level latency with the leader.
// The max throughput is around 10K. Keep a 5K entries is enough for helping
// follower to catch up.
DefaultSnapshotCatchUpEntries uint64 = 5000

另外,我们还发现,这个设计在历史上etcd的官方团队把这个默认值从10000降到了5000,我们估计etcd官方团队也意识到10000有点太耗内存了,所以,降了一半,但是又怕follwer同步不上,所以,保留了 5000条……(在这里,我个人感觉还有更好的方法,至少不用全放在内存里吧……)

另外还有下面几项也会导致etcd的内存会增加

  1. 索引。etcd的每一对 key-value 都会在内存中有一个 B-tree 索引。这个索引的开销跟key的长度有关,etcd还会保存版本。所以B-tree的内存跟key的长度以及历史版本号数量也有关系。
  2. mmap。还有,etcd 使用 mmap 这样上古的unix技术做文件映射,会把他的blotdb的内存map到虚拟内存中,所以,db-size越大,内存越大。
  3. Watcher。watch也会占用很大的内存,如果watch很多,连接数多,都会堆积内存。

(很明显,etcd这么做就是为了一个高性能的考虑)

Easegress中的问题更多的应该是Raft Log 的问题。后面三种问题我们觉得不会是用户这个问题的原因,对于索引和mmap,使用 etcd 的 compact 和 defreg (压缩和碎片整理应该可以降低内存,但用户那边不应该是这个问题的核心原因)。

针对用户的问题,大约有1000多条pipeline,因为Easegress会对每一条pipeline进行数据统计(如:M1, M5, M15, P99, P90, P50等这样的统计数据),统计信息可能会有1KB-2KB左右,但Easegress会把这1000条pipeline的统计数据合并起来写到一个key中,这1000多条的统计数据合并后会导致出现一个平均尺寸为2MB的key,而5000个in-memory的RaftLog导致etcd要消耗了10GB的内存。之前没有这么多的pipeline的场景,所以,这个内存问题没有暴露出来。

于是,我们最终的解决方案也很简单,我们修改我们的策略,不再写这么大的Value的数据了,虽然以前只写在一个key上,但是Key的值太大,现在把这个大Key值拆分成多个小的key来写,这样,实际保存的数据没有发生变化,但是RaftLog的每条数据量就小了,所以,以前是5000条 2M(10GB),现在是5000条 1K(500MB),就这样解决了这个问题。相关的PR在这里 PR#542

总结

要用好 etcd,有如下的实践

  • 避免大尺寸的key和value,一方面会通过一个内存级的 Raft Log 占大量内存,另一方面,B-tree的多版本索引也会因为这样耗内存。
  • 避免DB的尺寸太大,并通过 compact和defreg来压缩和碎片整理降低内存。
  • 避免大量的Watch Client 和 Watch数。这个开销也是比较大的。
  • 最后还有一个,就是尽可能使用新的版本,无论是go语言还是etcd,这样会少很多内存问题。比如:golang的这个跟LInux内核心相关的内存问题 —— golang 1.12的版sget的是 MADV_FREE 的内存回收机制,而在1.16的时候,改成了 MADV_DONTNEED ,这两者的差别是,FREE表示,虽然进程标记内存不要了,但是操作系统会保留之,直到需要更多的内存,而 DONTNEED 则是立马回收,你可以看到,在常驻内存RSS 上,前者虽然在golang的进程上回收了内存,但是RSS值不变,而后者会看到RSS直立马变化。Linux下对 MADV_FREE 的实现在某些情况下有一定的问题,所以,在go 1.16的时候,默认值改成了 MADV_DONTNEED 。而 etcd 3.4 是用 来1.12 编译的。

最后,欢迎大家关注我们的开源软件! https://github.com/megaease/ 

这两天技术圈里热议的一件事就是Amazon的流媒体平台Prime Video在2023年3月22日发布了一篇技术博客《规模化Prime Video的音视频监控服务,成本降低90%》,副标题:“从分布式微服务架构到单体应用程序的转变有助于实现更高的规模、弹性和降低成本”,有人把这篇文章在五一期间转到了reddithacker news 上,在Reddit上热议。这种话题与业内推崇的微服务架构形成了鲜明的对比。从“微服务架构”转“单体架构”,还是Amazon干的,这个话题足够劲爆。然后DHH在刚喷完Typescript后继续发文《即便是亚马逊也无法理解Servless或微服务》,继续抨击微服务架构,于是,瞬间引爆技术圈,登上技术圈热搜。

今天上午有好几个朋友在微信里转了三篇文章给我,如下所示:

看看这些标题就知道这些文章要的是流量而不是好好写篇文章。看到第二篇,你还真当 Prime Video 就是 Amazon 的全部么?然后,再看看这些文章后面的跟风评论,我觉得有 80%的人只看标题,而且是连原文都不看的。所以,我想我得写篇文章了……

原文解读

要认清这个问题首先是要认认真真读一读原文,Amazon Prime Video 技术团队的这篇文章并不难读,也没有太多的技术细节,但核心意思如下:

1)这个系统是一个监控系统,用于监控数据千条用户的点播视频流。主要是监控整个视频流运作的质量和效果(比如:视频损坏或是音频不同步等问题),这个监控主要是处理视频帧,所以,他们有一个微服务主要是用来把视频拆分成帧,并临时存在 S3 上,就是下图中的 Media Conversion 服务。

2)为了快速搭建系统,Prime Video团队使用了Serverless 架构,也就是著名的 AWS Lambda 和 AWS Step Functions。前置 Lambda 用来做用户请求的网关,Step Function 用来做监控(探测器),有问题后,就发 SNS 上,Step Function 从 S3 获取 Media Conversion 的数据,然后把运行结果再汇总给一个后置的 Lambda ,并存在 S3 上。

整个架构看上去非常简单 ,一点也不复杂,而且使用了 Serverless 的架构,一点服务器的影子都看不见。实话实说,这样的开发不香吗?我觉得很香啊,方便快捷,完全不理那些无聊的基础设施,直接把代码转成服务,然后用 AWS 的 Lamda + Step Function + SNS + S3 分分钟就搭出一个有模有样的监控系统了,哪里不好了?!

但是他们遇到了一个比较大的问题,就是 AWS Step Function 的伸缩问题,从文章中我看到了两个问题(注意前方高能):

  1. 需要很多很多的并发的 AWS Step Function ,于是达到了帐户的 hard limit。
  2. AWS Step Function 按状态转换收费,所以,贵得受不了了。

注意,这里有两个关键点:1)帐户对 Step Function 有限制,2)Step Function 太贵了用不起

然后,Prime Video 的团队开始解决问题,下面是解决的手段:

1) 把 Media Conversion  和 Step Function 全部写在一个程序里,Media Conversion 跟 Step Function 里的东西通过内存通信,不再走S3了。结果汇总到一个线程中,然后写到 S3.

2)把上面这个单体架构进行分布式部署,还是用之前的 AWS Lambda 来做入门调度。

EC2 的水平扩展没有限制,而且你想买多少 CPU/MEM 的机器由你说了算,而这些视频转码,监控分析的功能感觉就不复杂,本来就应该写在一起,这么做不更香吗?当然更香,比前面的 Serverless 的确更香,因为如下的几个原因:

  1. 不再受 Step Function 的限制了,技术在自己手里,有更大的自由度。
  2. 没有昂贵的 Step Function 云成本的确变得更低了,如果你把 Lambda 换成 Nginx 或 Spring Gateway 或是我司的 Easegress,你把 S3 换成 MinIO,你把 SNS 换成 Kafka,你的成本 还能再低。

独立思考

好了,原文解读完了,你有自己的独立思考了吗?下面是我的独立思考,供你参考:

1)AWS 的 Serverless 也好, 微服务也好,单体也好,在合适的场景也都很香。这就跟汽车一样,跑车,货车,越野车各有各的场景,你用跑车拉货,还是用货车泡妞都不是一个很好的决定。

2)这篇文章中的这个例子中的业务太过简单了,本来就是一两个服务就可以干完的事。就是一个转码加分析的事,要分开的话,就两个微服务就好了(一个转码一个分析),做成流式的。如果不想分,合在一起也没问题了,这个粒度是微服务没毛病。微服务的划分有好些原则,我这里只罗列几个比较重要的原则:

  • 边界上下文。微服务的粒度不能大于领域驱动里的 Bounded Context(具体是什么 大家自行 Google),也就是一个业务域。
  • 单一职责,高内聚,低耦合。把因为相同原因变化的合在一起(内聚),把不同原因变化的分开(解耦)
  • 事务和一致性。对于两个重度依赖的功能,需要完成一个事务和要保证强一致性的,最好不要拆开,要放在一起。
  • 跟组织架构匹配。把同一个团队的东西放在一起,不同团队的分开。

3)Prime Video 遇到的问题不是技术问题,而是 AWS  Step Function 处理能力不足,而且收费还很贵的问题。这个是 AWS 的产品问题,不是技术问题。或者说,这个是Prime Video滥用了Step Function的问题(本来这种大量的数据分析处理就不适合Step Function)。所以,大家不要用一个产品问题来得到微服务架构有问题的结论,这个没有因果关系。试问,如果 Step Funciton 可以无限扩展,性能也很好,而且白菜价,那么 Prime Video 团队还会有动力改成单体吗?他们不会反过来吹爆 Serverless 吗?

4)Prime Video 跟 AWS 是两个独立核算的公司,就像 Amazon 的电商和 AWS 一样,也是两个公司。Amazon 的电商和 AWS 对服务化或是微服务架构的理解和运维,我个人认为这个世界上再也找不到另外一家公司了,包括 Google 或 Microsoft。你有空可以看看本站以前的这篇文章《Steve Yegg对Amazon和Google平台的吐槽》你会了解的更多。

5)Prime Video 这个案例本质上是“下云”,下了 AWS Serverless 的云。云上的成本就是高,一个是费用问题,另一个是被锁定的问题。Prime Video 团队应该很庆幸这个监控系统并不复杂,重写起来也很快,所以,可以很快使用一个更传统的“服务化”+“云计算”的分布式架构,不然,就得像 DHH 那样咬牙下云——《Why We’re Leaving the Cloud》(他们的 SRE 的这篇博文 Our Cloud Spend in 2022说明了下云的困难和节约了多少成本)

后记

最后让我做个我自己的广告。我在过去几年的创业中,帮助了很多公司解决了这些 分布式,微服务,云原生以及云计算成本的问题,如果你也有类似问题。欢迎,跟我联系:[email protected]

另外,我们今年发布了一个平台 MegaEase Cloud,就是想让用户在不失去云计算体验的同时,通过自建高可用基础架构的方式来获得更低的成本(至少降 50%的云计算成本)。目前可以降低成本的方式:

  1. 基础软件:通过开源软件自建,
  2. 内容分发:MinIO + Cloudflare 的免费 CDN,
  3. 马上准备发布的直接与底层IDC合作的廉价GPU计算资源…

欢迎大家试用。

如何访问

注:这两个区完全独立,帐号不互通。因为网络的不可抗力,千万不要跨区使用。

产品演示

介绍文章

 

今天跟大家分享一个etcd的内存大量占用的问题,这是前段时间在我们开源软件Easegress中遇到的问题,问题是比较简单的,但是我还想把前因后果说一下,包括,为什么要用etcd,使用etcd的用户场景,包括etcd的一些导致内存占用比较大的设计,以及最后一些建议。希望这篇文章不仅仅只是让你看到了一个简单的内存问题,还能让你有更多的收获。当然,也欢迎您关注我们的开源软件,给我们一些鼓励。

为什么要用ETCD

先说一下为什么要用etcd。先从一个我们自己做的一个API网关 – Easegress(源码)说起。

Easegress 是我们开发并开源的一个API应用网关产品,这个API应用网关不仅仅只是像nginx那样用来做一个反向代理,这个网关可以做的事很多,比如:API编排、服务发现、弹力设计(熔断、限流、重试等)、认证鉴权(JWT,OAuth2,HMAC等)、同样支持各种Cloud Native的架构如:微服务架构,Service Mesh,Serverless/FaaS的集成,并可以用于扛高并发、灰度发布、全链路压力测试、物联网……等更为高级的企业级的解决方案。所以,为了达到这些目标,在2017年的时候,我们觉得在现有的网关如Nginx上是无法演进出来这样的软件的,必需重新写一个(后来其他人也应该跟我们的想法一样,所以,Lyft写了一个Envoy。只不过,Envoy是用C++写的,而我用了技术门槛更低的Go语言)

另外,Easegress最核心的设计主要有三个:

  • 一是无第三方依赖的自己选主组集群的能力
  • 二是像Linux管道命令行那样pipeline式的插件流式处理(支持Go/WebAssembly)
  • 三是内置一个Data Store用于集群控制和数据共享。

对于任何一个分布式系统,都需要有一个强一制性的基于Paxos/Raft的可以自动选主机制,并且需要在整个集群间同步一些关键的控制/配置和相关的共享数据,以保证整个集群的行为是统一一致的。如果没有这么一个东西的话,就没有办法玩分布式系统的。这就是为什么会有像Zookeeper/etcd这样的组件出现并流行的原因。注意,Zookeeper他们主要不是给你存数据的,而是给你组集群的。

Zookeeper是一个很流行的开源软件,也被用于各大公司的生产线,包括一些开源软件,比如:Kafka。但是,这会让其它软件有一个依赖,并且在运维上带来很大的复杂度。所以,Kafka在最新的版本也通过内置了选主的算法,而抛弃了外挂zookeeper的设计。Etcd是Go语言社区这边的主力,也是kubernetes组建集群的关键组件。Easegress在一开始(5年前)使用了gossip协议同步状态(当时想的过于超前,想做广域网的集群),但是后发现这个协议太过于复杂,而且很难调试,而广域网的API Gateway也没遇到相应的场景。所以,在3年前的时候,为了稳定性的考量,我们把其换成了内嵌版本的etcd,这个设计一直沿用到今天。

Easegress会把所有的配置信息都放到etcd里,还包括一些统计监控数据,以及一些用户的自定义数据(这样用户自己的plugin不但可以在一条pipeline内,还可以在整个集群内共享数据),这对于用户进行扩展来说是非常方便的。软件代码的扩展性一直是我们追求的首要目标,尤其是开源软件更要想方设法降低技术门槛让技术易扩展,这就是为什么Google的很多开源软件都会选使用Go语言的原因,也是为什么Go正在取代C/C++的做PaaS基础组件的原因。

背景问题

好了,在介绍完为什么要用etcd以后,我开始分享一个实际的问题了。我们有个用户在使用 Easegress 的时候,在Easegress内配置了上千条pipeline,导致 Easegress的内存飙升的非常厉害- 10+GB 以上,而且长时间还下不来。

用户报告的问题是——

在Easegress 1.4.1 上创建一个HTTP对象,1000个Pipeline,在Easegres初始化启动完成时的内存占用大概为400M,运行80分钟后2GB,运行200分钟后达到了4GB,这期间什么也没有干,对Easegress没有进行过一次请求。

一般来说,就算是API再多也不应该配置这么多的处理管道pipeline的,通常我们会使用HTTP API的前缀把一组属于一个类别的API配置在一个管道内是比较合理的,就像nginx下的location的配置,一般来说不会太多的。但是,在用户的这个场景下配置了上千个pipeline,我们也是头一次见,应该是用户想做更细粒度的控制。

经过调查后,我们发现内存使用基本全部来自etcd,我们实在没有想到,因为我们往etcd里放的数据也没有多少个key,感觉不会超过10M,但不知道为什么会占用了10GB的内存。这种时候,一般会怀疑etcd有内存泄漏,上etcd上的github上搜了一下,发现etcd在3.2和3.3的版本上都有内存泄露的问题,但都修改了,而 Easegress 使用的是3.5的最新版本,另外,一般来说内存泄漏的问题不会是这么大的,我们开始怀疑是我们哪里误用了etcd。要知道是否误用了etcd,那么只有一条路了,沉下心来,把etcd的设计好好地看一遍。

大概花了两天左右的时间看了一下etcd的设计,我发现了etcd有下面这些消耗内存的设计,老实说,还是非常昂贵的,这里分享出来,避免后面的同学再次掉坑。

首当其冲是——RaftLog。etcd用Raft Log,主要是用于帮助follower同步数据,这个log的底层实现不是文件,而是内存。所以,而且还至少要保留 5000 条最新的请求。如果key的size很大,这 5000条就会产生大量的内存开销。比如,不断更新一个 1M的key,哪怕是同一个key,这 5000 条Log就是 5000MB = 5GB 的内存开销。这个问题在etcd的issue列表中也有人提到过  issue #12548 ,不过,这个问题不了了之了。这个5000还是一个hardcode,无法改。(参看 DefaultSnapshotCatchUpEntries 相关源码

// DefaultSnapshotCatchUpEntries is the number of entries for a slow follower
// to catch-up after compacting the raft storage entries.
// We expect the follower has a millisecond level latency with the leader.
// The max throughput is around 10K. Keep a 5K entries is enough for helping
// follower to catch up.
DefaultSnapshotCatchUpEntries uint64 = 5000

另外,我们还发现,这个设计在历史上etcd的官方团队把这个默认值从10000降到了5000,我们估计etcd官方团队也意识到10000有点太耗内存了,所以,降了一半,但是又怕follwer同步不上,所以,保留了 5000条……(在这里,我个人感觉还有更好的方法,至少不用全放在内存里吧……)

另外还有下面几项也会导致etcd的内存会增加

  1. 索引。etcd的每一对 key-value 都会在内存中有一个 B-tree 索引。这个索引的开销跟key的长度有关,etcd还会保存版本。所以B-tree的内存跟key的长度以及历史版本号数量也有关系。
  2. mmap。还有,etcd 使用 mmap 这样上古的unix技术做文件映射,会把他的blotdb的内存map到虚拟内存中,所以,db-size越大,内存越大。
  3. Watcher。watch也会占用很大的内存,如果watch很多,连接数多,都会堆积内存。

(很明显,etcd这么做就是为了一个高性能的考虑)

Easegress中的问题更多的应该是Raft Log 的问题。后面三种问题我们觉得不会是用户这个问题的原因,对于索引和mmap,使用 etcd 的 compact 和 defreg (压缩和碎片整理应该可以降低内存,但用户那边不应该是这个问题的核心原因)。

针对用户的问题,大约有1000多条pipeline,因为Easegress会对每一条pipeline进行数据统计(如:M1, M5, M15, P99, P90, P50等这样的统计数据),统计信息可能会有1KB-2KB左右,但Easegress会把这1000条pipeline的统计数据合并起来写到一个key中,这1000多条的统计数据合并后会导致出现一个平均尺寸为2MB的key,而5000个in-memory的RaftLog导致etcd要消耗了10GB的内存。之前没有这么多的pipeline的场景,所以,这个内存问题没有暴露出来。

于是,我们最终的解决方案也很简单,我们修改我们的策略,不再写这么大的Value的数据了,虽然以前只写在一个key上,但是Key的值太大,现在把这个大Key值拆分成多个小的key来写,这样,实际保存的数据没有发生变化,但是RaftLog的每条数据量就小了,所以,以前是5000条 2M(10GB),现在是5000条 1K(500MB),就这样解决了这个问题。相关的PR在这里 PR#542

总结

要用好 etcd,有如下的实践

  • 避免大尺寸的key和value,一方面会通过一个内存级的 Raft Log 占大量内存,另一方面,B-tree的多版本索引也会因为这样耗内存。
  • 避免DB的尺寸太大,并通过 compact和defreg来压缩和碎片整理降低内存。
  • 避免大量的Watch Client 和 Watch数。这个开销也是比较大的。
  • 最后还有一个,就是尽可能使用新的版本,无论是go语言还是etcd,这样会少很多内存问题。比如:golang的这个跟LInux内核心相关的内存问题 —— golang 1.12的版sget的是 MADV_FREE 的内存回收机制,而在1.16的时候,改成了 MADV_DONTNEED ,这两者的差别是,FREE表示,虽然进程标记内存不要了,但是操作系统会保留之,直到需要更多的内存,而 DONTNEED 则是立马回收,你可以看到,在常驻内存RSS 上,前者虽然在golang的进程上回收了内存,但是RSS值不变,而后者会看到RSS直立马变化。Linux下对 MADV_FREE 的实现在某些情况下有一定的问题,所以,在go 1.16的时候,默认值改成了 MADV_DONTNEED 。而 etcd 3.4 是用 来1.12 编译的。

最后,欢迎大家关注我们的开源软件! https://github.com/megaease/