阅读视图

发现新文章,点击刷新页面。

字节跳动观测数据埋点标准化实践

来源|字节跳动基础架构-可观测团队

背景

随着字节跳动业务规模不断扩大,对存量和新增业务的服务质量承诺变得越发关键。稳定性治理方面:怎样支持保障服务线上的高可用性,或者在出现故障/事故时,如何高效且迅速地止损、定位分析影响面已成为一个重要议题。

稳定性建设所涉及的话题十分广泛,涵盖流程梳理与标准化、数据标准化、SLO 定义、故障自愈、事故复盘和演练等方面,字节跳动基础架构可观测团队提供的稳定性平台建设思路是“事前预防、事中处理、事后复盘、事后补救/反哺事前”这四个阶段。

其中, 观测数据标准化以及一系列配套的数据链路,如:数据埋点、数据消费、数据存储、数据查询、离线数仓等,都是后续稳定性建设的重要数据基石。

并且,由此引申出排障/止损效率的问题,由于字节的服务/基础设施是分层建设的,包括端侧客户体验层、网络接入层、应用服务层、基础设施层、IDC\资源层等,不同层面的统计/描述口径是否一致、能否对应,以达到在跨层间能上卷下钻和平层内过滤聚合的“车同轨书同文”效果,这对于大幅提升整体排查效率, 让 SRE/GOC 同学能够自助完成端到端的问题排查就显得尤为重要。

img_v3_02ik_a98c1a06-a522-493e-a443-78169a1b9f3g.png

拥有统一的观测数据标准, 能够在很大程度上提升团队间的排障效率,从人工分析的方式提升至更大程度的自助/自动化排障的阶段。

埋点标准化的重要性

提高研发效率 & 降低研发协同成本

  • 面向排障方面:跨层间的上下文过滤便捷,术语统一。
  • 进行历史数仓分析(容量优化)时,整体数据处理逻辑的适配成本会大幅降低。
  • 用户的学习曲线陡峭,理解心智负担沉重。

为 AIOps 提供强有力的数据支撑

观测数据属于 AIOps 的五大基石(数据、知识、算法、代码联动、人机协同)之一。在清华裴丹老师的《AIOps 落地的 15 条原则》里,也都提及了数据的重要性。

拥有数据标准化和统一的访问体验,为后续稳定性的终极目标 MTTR 1-5-10(1 分钟发现,5 分钟响应以及 10 分钟快恢复)提供了数据层面的保障。包括同层数据的聚合 / 过滤,以及跨层数据的下钻和上卷,都会有统一的使用方式。

名词解释

名词 解释
Metrics 2.0 字节跳动内部使用广泛的时序数据处理引擎,提供了时序数据收集、存储和聚合查询的功能。2.0 版本提供引入多值概念,打平prometheus 4类指标类型语义、支持秒级打点& 存储周期定制化等多租户特性、 端到端高性能优化的分布式时序集群版本。
BytedTrace BytedTrace是字节跳动使用的一套集成了 Tracing/Logging/Metrics 三种能力的可观测性解决方案,提供了从采集、传输、存储、检索到前端产品化交互的整套能力。它定义了统一的数据模型(Trace 、Span 、Event、Metrics 等),提供了各语言配套 SDK,并与公司各主流框架组件实现默认集成。
观测埋点 TagKV Metrics TagKV 是一种用于标记和管理度量数据的键值对(Key-Value Pair)格式。通常用于监控系统、分布式追踪系统和日志管理系统等领域,TagKV 提供了一种灵活且高效的方法来分类和筛选数据。
Measurement 可观测对象的某个指标,如服务的上游调用延时,物理机的 CPU 使用率。Measurement 是带有可观测对象的 context的,是语义化的,同时能识别在不同条件下应该使用哪个版本的指标以及对应的 TagKV。而且可以根据观测对象的元数据条件,同时关联多个时序数据源,便于按需时序数据源切换。
SLO Service Level Objectives,服务级目标是指服务提供方对所提供服务的某些性能或质量指标所设定的目标值。这些指标通常用于衡量服务的可用性、性能和其他关键属性,旨在确保服务达到预期的质量水平。
TCE Toutiao Cloud Engine,为字节跳动内部提供的高度可用、弹性扩展的容器服务。
PSM Product Subsys Module,是字节跳动内部服务的唯一标识。
GOC Global Operations Center,基于字节跳动各类研发,运维体系下的高可用产品能力,结合稳定性保障策略及运营机制,提供字节跳动全线基础产品的可靠性服务与设施稳定性保障,达成字节跳动全线业务各类场景下的端到端高可用性。

字节埋点标准化挑战与拆解思路

挑战: 历史上可观测性埋点质量偏低

首先,我们对埋点标准化进行定义,包括但不仅限于如下的标准定义,包括覆盖完整、定义统一、计量准确、面向引擎友好等四大方面。

img_v3_02ik_56f19e79-13a2-4bdf-aa70-2d6d49e02b8g.png

简而言之,在 2020 年以前,从覆盖完整定义统一计量准确面向引擎友好等维度来看,字节整体的观测数据埋点存在一定的差距。

具体如下:

  • 负载均衡 埋点

    • 计量准确:中等水平

      • 存在较严重的打点丢失问题
    • 面向引擎友好:较低水平

      • 指标打点对于配置预计算不友好
      • 指标名膨胀也比较严重
  • 微服务 埋点

    • 覆盖完整:中等水平

      • 20 年前 Tracing 方案还在 V1 版本
    • 计量准确:中等水平

      • 遇到高基数的指标会被封禁
    • 面向引擎友好:较低水平

      • 指标打点对于配置预计算不友好
      • 指标名膨胀也比较严重
      • 加权计算也不好实现
  • 语言 运行时 埋点

    • 定义统一:较低水平

      • Golang & C++ 框架 不同的版本定义的指标格式都不太一样
    • 面向引擎友好:较低水平

      • 指标打点对于配置预计算不友好
  • 容器指标 埋点

    • 覆盖完整:较低水平

      • 没有日志采集覆盖
    • 计量准确:中等水平

      • 遇到高基数的指标会被封禁
    • 面向引擎友好:较低水平

      • 指标打点对于配置预计算不友好
  • 基础架构 存储 & 数据库 埋点

    • 覆盖完整:较低水平

      • 存储、数据库、MQ 客户端没有黄金指标打点
      • 没有日志采集覆盖
    • 计量准确:较低水平

      • 不同存储、数据库、MQ 产品打点格式 都不一
    • 面向引擎友好:较低水平

      • 指标打点对于配置预计算不友好

思路: 分层&向后兼容推进埋点标准化

总结来说,之前的字节服务端观测数据质量大致存在三类问题。

  • 同层数据/跨层数据不一致。
  • 观测的多模态数据类型(指标、日志、链路)的数据定义不统一。
  • 观测数据格式对引擎不够友好,例如所有数据都在 default 租户的一个大仓里,再比如很多观测指标的定义对于预计算不友好。

针对上述问题,我们采取了如下的多个思路逐一进行解决。

实施思路

一方面,在埋点侧就尽可能统一埋点 TagKV 定义,而且平台级 TagKV 都通过环境变量或者请求上下文自动注入对应的 Tag Value, 以防止由业务手工注入带来的人工错误。

另一方面,对于指标、链路和日志侵入式 SDK,我们通过字节内部的远程过程调用框架以及存储、数据库、消息中间件的客户端 SDK 搭载嵌入中间件,对于业务来说,能相对透明地升级到最新特性的版本。另外, 对于远远低于 SDK 基线版本的服务, 我们也通过字节软件供应链安全治理平台通过编译卡点的不同程度[warning 提示/发布卡点]推动业务升级。

在 负载均衡、应用、中间件、存储计算组件等各个纵向方面, 我们也主动与对应的平台对接,推动指标、日志、链路的埋点注入。

最后,在指标埋点上也额外关注对于多租户的声明,以达到一定的分库分表功能,以及多值声明,以最大程度减少数据消费和存储成本。如下所示, 就是团队在各个不同观测对象的埋点方面所做的业务推进情况。

img_v3_02ik_82b1a459-030a-43f1-8db0-808d5cc209eg.jpg

难点: 识别和解决

类似观测数据标准化的工作历经多年,牵涉的团队众多,整个过程并非毫无波折。遇到问题时要解决问题并思考能否将其标准化或者平台化,同时也要考虑能否尽可能地复用其他团队的能力和工具来助力我们进一步推广。当时如何高效地推动业务升级是我们的主要目标。

[业务推进] 高效推动业务升级观测SDK

在 Metrics SDK 需要升级到基线版本的情况下,以前的做法是在字节软件供应链安全治理平台上配置版本拦截,提醒用户升级,但是整体升级效率比较低,我们也无法跟踪用户的升级进展。因此我们联合字节软件供应链安全治理平台团队实现 SDK 自动升级功能。

Metrics ****SDK 自动升级

Metrics ****SDK 自动升级功能可以自动实现在当前业务代码库的代码提交期间,如果检测到对应集成的metrics SDK 低于基线版本,则会向用户推送代码提交失败的通知,提醒用户需要主动升级到metrics SDK基线版本或以上的操作。

远程过程调用 框架 & 基础组件客户端 集成 ****BytedTrace ****SDK 集成

观测团队多年来持续推动公司的远程过程调用 框架以及基础组件客户端 集成 BytedTrace SDK **** ****借助字节软件供应链安全治理平台进行递进式卡点推广,依靠代码血缘平台来推动框架、组件的基础库版本实现升级。在存有流量的微服务上,BytedTrace SDK的覆盖比例按照 TCE pod 接入情况来计算,当前已达到 95%。

从服务的优先级角度而言,公司当前96% 的 P0 服务中已接入 Bytedtrace SDK 。

[业务推进] 提升基础组件观测埋点质量

TCE 调度 / 运行时 打点格式设计思路

前文提到,提升业务层、应用层、容器层等多层间指标的跨层关联和下钻能力是指标标准化的一个重要目标,而实现跨层关联的关键动作在于保证同一含义的指标 TagKV 在各层上的定义保持统一,为实现这一点,我们对各个层次上的核心组件进行了统一的设计,具体如下:

层次 核心组件/着手点 埋点标准化设计思路
业务层 Metrics 2.0 SDK - 内置统一的平台级TagKV,提供横向跨语言、跨服务的TagKV统一
应用层 运行时 指标、远程过程调用 指标 - 横向上,提供统一的、跨语言的指标名定义
  • 纵向上,对齐Metrics 2.0 SDK 平台级TagKV规范 | | 容器层 | 与调度合作,对容器指标采集agent(TCE调度)进行标准化改造 | - 对齐Metrics 2.0 SDK 平台级TagKV规范 |
  1. 首先,我们在 Metrics 2.0 SDK 内置定义了一套平台级 TagKV,这样所有使用 Metrics 2.0 SDK 的业务打点都会携带标准的预定义的 TagKV。这些共同TagKV包括: _cluster、_psm、_pod_name、_ipv4 等。

  2. 在应用层,挑选了对业务排障、应用观测常用且通用的两类指标(运行时、远程过程调用)进行标准化,目标是在横向上,提供跨语言、统一的指标名、TagKV语义定义等;在纵向上,对齐 Metrics 2.0 SDK 平台级 TagKV 规范,以便于跨层关联。以 运行时 指标为例,其定义规范如下:

    1. 不同语言的指标采用统一命名约定:runtime. {runtime} . {metric}[ . {field}]
    2. 不同语言类似含义指标采用统一命名风格:如 go、java 中统计堆对象申请量的指标都命名为memory.allocated_bytes
    3. 必须包含 Metrics 2.0 SDK TagKV 规范的平台级 TagKV,如 _psm、_pod_name 等
  3. 在容器层,与调度团队共同推动其 TCE 容器指标采集 agent(TCE调度) 的指标标准化改造,指标 TagKV 对齐Metrics 2.0 SDK TagKV 规范。

通过将这些核心组件进行标准化改造,为跨层的指标关联和下钻提供了能力基础。同时,在每个核心组件的指标定义上,我们还通过以下两个方式进一步提升埋点的性能和成本收益,第一点是对各个组件使用独立租户,实现资源的隔离,保障写入稳定性和查询性能;

指标 租户名 集群类型
运行时 apm.runtime 独立集群
远程过程调用 框架 apm.rpc 独立集群
TCE 容器指标 computation.tce 独立集群

第二点是在语义明确的前提下,尽量使用多值格式定义指标,降低存储成本。以 TCE调度 指标为例,将原来 mem 相关的四个指标合并为一个多值指标的四个字段,存储成本大致可以被认为降低至四分之一。

原指标 改造后多值指标名 改造后多值字段
tce.host.mem_total inf.tce.host.mem total
tce.host.mem_free free
tce.host.mem_available available
tce.host.mem_used used

[配套工具] 帮助平滑迁移观测数据

[工具1] 语义 化指标替换

我们提供语义化指标替换,称为Measurement,其能力就是对原始 Metrics 打点的语义化封装;同时能识别在不同条件下应该使用哪个版本的指标以及对应的 TagKV。这两个关键能力能够促使在做数据迁移时,观测大盘和报警基本达到比较平滑的状态。

原始 Metrics 打点:直接写入时序数据库(可以是 metrics \ influxdb \ prometheus)的数据。

语义 封装:用标准的语义化来包装原始的 metrics 打点数据。 比如 go 服务的 gc 数量的 metrics 打点是 go.{{.psm}}.numGcs,其中{{.psm}}为具体的 psm, 我们会定制一个语义化指标名叫 "runtime.go.gc_num"来表达 go 服务的 gc 数量,包括用统一的 TagKV 来封装对应的原始 TagKV。 不管是 open api 还是前端调用, 都用指标 "runtime.go.gc_num" 对measurement 服务进行调用。

不同条件下的查询 路由:需要这个能力是因为在字节内部原始 Metrics 的打点会不断的升级, 比如 golang 运行时 历史上会有 v1 、v2 、v3 多个版本,我们需要能够在给定的输入信息条件下去查询到对应的指标版本。这个判断条件实现的逻辑一般为可用输入的 psm 名字构成 Metrics go v1 的指标名,再根据指标名的数据是否存在来判断是 runtime v1、runtime v2 或者 runtime v3 的版本,指标判断也以此类推。或者可以通过 psm 的 scm 编译信息确定该 psm 编译的 golang 运行时 版本是 v1、v2 或者 v3。 通过对应条件的判断来做到对应数据的查询路由。

img_v3_02ik_5c2f9415-8b2f-4dd5-a031-dff8ce63af6g.png

在有了 Measurement 能力后,我们抽象出了 Measurement 服务,该服务作为观测大盘和报警的一个数据源。在尽量不需要用户介入的情况下完成数据打点的迁移和替换。

当前借助 Measurement 能力,针对公司的 远程过程调用、HTTP 等框架,容器引擎、FaaS、机器学习推理等平台,还有负载均衡、缓存、数据库、消息队列等基础组件,以及golang 运行时 等,均进行了统一的标准化语义封装,这些语义化封装在观测平台上均有所展现。

[工具2] Metrics 前缀分流

怎样帮助业务顺利地迁移到新租户,同时确保新老指标的查询方式均可使用,是我们在推动业务租户迁移时所面临的较大挑战。

针对上述问题,观测团队起初推进引导用户主动迁移至新租户,旨在实现租户隔离,提供更优的稳定性保障,进行精细化容量治理以降低成本。然而,后来发现主动迁移的速度太慢,赶不上打点量的自然增长。于是,推出了让用户无感知的被动租户迁移方案。大致思路是依据某些特定的指标前缀,主要涵盖一级 / 二级前缀,通过特定配置把这些指标分别路由到不同的新租户,并且在新租户上支持查询翻译,即便用户不修改查询租户,继续用 Default 租户查询仍能正常获取数据。该方案具有以下优势:

  1. 业务在读写两侧无需进行代码变更,就能将流量迁移到新租户集群。
  2. 最大程度减少不同租户间因集群变更和读写流量变化对线上稳定性产生的相互影响,提供更出色的稳定性保障。
  3. 精准对接业务线租户,便于后续进行打点流量治理、容量规划以及资源充值等操作。

具体的实现由 Metrics 组件中各模块的相互配合完成,包括写入、控制面、查询、数仓等方面,大致的实现流程如下:

前缀分流租户的整个过程存在众多细节,为减少过程中的过多人为操作,防止出现某些环节被遗忘的情况,观测团队设计了分流流程工单以及白屏化运维平台,尽可能让整个操作流程实现自动化,提高分流租户的效率。此外,前缀分流迁移新租户的整个过程对于业务来说成本为零,同时对于 观测团队而言不像依赖业务方主动迁移那样周期漫长,其周期短、生效时间快,能够收敛团队人力的持续投入。

总的来说,观测团队提供了一种让用户无感知、实现无缝迁移新租户的方案,用户的核心观测大盘和报警也无需修改,最大程度降低了埋点标准化对用户的打扰。

埋点标准化字节的实践与效果

观测数据质量前后对比

经过 2020-2022 年推进 BytedTrace SDK 覆盖率、2023 年推动云基础组件和应用层指标租户迁移之后, 从埋点标准化的 4 个维度看,都有不同程度的质量提升。

  • 负载均衡

    • 计量准确:较高水平 [2020年为中等水平]

      • 通过 2.0 SDK 三个特性, 基本消除丢点的问题:

        • 打点本地聚合
        • 面向字节流的 codec 编码
        • Agentless 投递
    • 面向引擎友好:较高水平 [2020年为较低水平]

      • 实现面向预计算友好的效果
    • 成本收益:

      • Metrics 2. 0 打点商品成本相对 1.0 下降 94%
      • Metrics 2. 0 很好地解决了打点封禁问题,特别是在一些配置量巨大的核心集群,解决了其超过 90%打点无法查询的情况
      • Metrics2. 0 TLB 机器成本初步统计主容器和 adaptor 打平,同时相对 1.0 节约了 ms2 的 15000 核资源
  • 微服务

    • 覆盖完整:较高水平 [2020年为中等水平]

      • 80%以上 PSM 覆盖到 BytedTrace SDK 集成
    • 计量准确:中等偏上水平 [2020年为中等水平]

      • 高基数的指标封禁问题 由于迁移到了新租户 可以做封禁阈值定制化
      • [计划中] 升级 bytedTrace 内的 metrics 2.0 SDK 降低丢点的风险
    • 面向引擎友好:较高水平 [2020年为较低水平]

      • 实现面向预计算友好的效果
    • 成本收益:

      • 以计算关键组件 Consumer 为例,新租户只需要老租户 20%的资源,就可以完成相同数据的写入计算;其他写入计算类组件也类似
      • 以存储关键组件 tsdc 为例,新租户只需要老租户 55%的资源,就可以完成数据的写入、存储
  • 语言 运行时

    • 定义统一:较高水平 [2020年为较低水平]

      • 统一了不同语言和框架的 运行时 打点格式
  • 容器指标

    • 覆盖完整:中等水平 [2020年为较低水平]

      • TCE调度 接入日志租户
    • 计量准确:较高水平 [2020年为中等水平]

      • 引入多值 降低指标名数量

      • 高基数的指标封禁问题 由于迁移到了新租户 可以做封禁阈值定制化

      • 通过 2.0 SDK 三个特性, 基本消除丢点的问题

        • 打点本地聚合
        • 面向字节流的 codec 编码
        • Agentless 投递
    • 面向引擎友好:较高水平 [2020年为较低水平]

      • 实现面向预计算友好的效果
  • 基础架构 存储 & 数据库

    • 计量准确:较高水平 [2020年为中等水平]

      • 引入多值 降低指标名数量

      • 高基数的指标封禁问题 由于迁移到了新租户 可以做封禁阈值定制化

      • 通过 2.0 SDK 三个特性, 基本消除丢点的问题

        • 打点本地聚合
        • 面向字节流的 codec 编码
    • 面向引擎友好:中等水平 [2020年为较低水平]

      • 打点格式调整的 支持预计算配置
    • 成本收益:

      • 以 mysql 迁移为例

        • Mysql 租户 成本节省 45.7%
        • Mysql 租户 带宽节省了 80%

截止到今年年初, Metrics 在中国国内区域已经接入 60+ 租户,占总流量的 70% 左右。

赋能效果总结

加速微服务端到端根因定位

通过指标标准化 & 多模观测数据 [指标, 日志,链路]标签术语的标准化, 我们实现面向微服务的上卷 & 下钻关联分析。

也使得使得跨层问题根因分析有了可能性:

目前端到端根因定位覆盖了60%以上的报警场景,日均触发根因定位 50余万 次,用户对定位结果的正反馈率超过80%。

简化服务性能离线数仓构建

在实现了在线观测数据的标准化,并将其导入统一的存储介质之后,构建字节整体关于服务性能、容量、吞吐量的数仓大盘就更加便捷。比如 展现某服务的单核 QPS 分时热力图 如下:

目前基于微服务应用性能数仓已覆盖公司超97%的微服务量化,有效支持字节跳动各业务线服务性能、服务应用健康度度量,由此带动一系列精准的成本优化。

观测底座自身收益

  • 从稳定性角度看,由于引入metrics多租户概念,所以我们能够通过逻辑租户映射到物理资源,从而降低故障半径,减少不同租户间流量的相互干扰。
  • 从成本角度看,我们能够依据每个租户的副本数、存储时长 TTL、打点的最小精度以及多值定义,最大程度地降低写入流量和存储容量的成本。metrics 多租户迁移前后对比,成本节省幅度在 20% ~ 80% 不等。

总结

历经上述观测埋点套件 BytedTrace SDK推广、Metrics 指标标准化迁移和推广、部分业务接入日志多租户,字节后端观测数据的质量在覆盖完整度定义统一计量准确面向引擎友好四个方面上取得了显著的质量提升。这也为后续的全景全栈高效排障奠定了坚实的基础,帮助更多业务团队在业务稳定性方向持续建设。


依托字节跳动内部可观测团队大规模技术实践,通过内外合力,在火山引擎上推出了应用性能监控全链路版(APMPlus)、托管 Prometheus(VMP)、云监控等可观测产品,致力于为用户提供全面、智能、高效、易用且安全的全栈可观测解决方案。

目前 APMPlus Server 端监控已正式 GA 并支持最新的大模型链路追踪相关能力,欢迎咨询了解。

🔗 相关链接

APMPlus www.volcengine.com/product/apm…

VMP www.volcengine.com/product/pro…

云监控 www.volcengine.com/product/clo…

在 JavaScript 中处理中文字符串的 Base64 编码与解码

在 JavaScript 中处理中文字符串的 Base64 编码与解码

在 Web 开发中,Base64 编码是一种常见的数据转换方式。它通过将二进制数据转换为文本数据,便于在 HTTP 请求、URL 中传输或存储。然而,处理中文字符时,Base64 编码会面临一些特殊的挑战,因为中文字符通常占用多个字节,而 Base64 编码是基于字节的。在本文中,我们将介绍两种方法来处理中文字符串的 Base64 编码与解码。


为什么 Base64 编码会与中文字符发生冲突?

Base64 编码是一种将二进制数据转换为 ASCII 字符串格式的方式。每个字符占用 6 位,而 Base64 编码本身的原理是将输入数据按照每 6 位拆分,并映射到对应的 Base64 字符上。

然而,中文字符在 UTF-8 编码下通常需要多个字节(通常是 3 个字节)。因此,如果我们直接对中文字符进行 Base64 编码而没有正确处理字符编码,可能会导致编码错误或者解码时出现乱码。为了避免这个问题,我们需要先将中文字符转换为 UTF-8 字节,然后再进行 Base64 编码。

在本文中,我们将介绍两种不同的方式来解决这个问题,分别是手动实现 Base64 编码和解码的方式,以及使用 TextEncoderTextDecoder API 来处理编码和解码。


方法一:手动实现 Base64 编码与解码

在这个方法中,我们手动处理每个字节,并将字节转化为二进制字符串。然后,我们将二进制字符串按每 6 位进行拆分,映射为 Base64 字符。解码时,我们通过反向操作将 Base64 字符还原为二进制数据,然后恢复为原始的 UTF-8 字符串。

(1) 中文字符串的 Base64 编码
function base64Encode(input) {
    const base64Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';

    // 将字符串转换为 UTF-8 字节
    let utf8Bytes = new TextEncoder().encode(input);

    // 将每个字节转换为二进制字符串
    let binaryString = '';
    for (let i = 0; i < utf8Bytes.length; i++) {
        binaryString += utf8Bytes[i].toString(2).padStart(8, '0');
    }

    // 按 6 位拆分
    const chunks = [];
    for (let i = 0; i < binaryString.length; i += 6) {
        chunks.push(binaryString.slice(i, i + 6));
    }

    // 如果最后一组少于 6 位,进行填充
    if (chunks[chunks.length - 1].length < 6) {
        chunks[chunks.length - 1] = chunks[chunks.length - 1].padEnd(6, '0');
    }

    // 查找对应的 Base64 字符
    let base64Encoded = chunks.map(chunk => {
        const index = parseInt(chunk, 2); // 将二进制转换为数字
        return base64Chars.charAt(index);
    }).join('');

    // 添加填充字符
    while (base64Encoded.length % 4 !== 0) {
        base64Encoded += '=';
    }

    return base64Encoded;
}

// 示例
const input = '你好润,Base64!';
const encoded = base64Encode(input);
console.log('Encoded:', encoded);// Encoded: 5L2g5aW95ram77yMQmFzZTY077yB
解释:
  • UTF-8 编码:通过 TextEncoder().encode(input) 方法,我们将输入的中文字符串转换为 UTF-8 字节。每个字节在 UTF-8 编码中通常会占用多个字节(例如中文字符通常占用 3 个字节)。
  • 二进制转换:我们将每个字节转为二进制字符串,并将它们合并成一个长的二进制串。
  • Base64 编码:通过按每 6 位将二进制字符串拆分,我们将每一组 6 位的二进制数转换为对应的 Base64 字符。
  • 填充字符:Base64 编码要求输出的字符数必须是 4 的倍数,因此我们在末尾添加 = 字符进行填充。
(2) 中文字符串的 Base64 解码
function base64Decode(input) {
    const base64Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';

    // 去除填充字符 '='
    input = input.replace(/=/g, '');

    // 将每个 Base64 字符转换为 6 位二进制
    let binaryString = '';
    for (let i = 0; i < input.length; i++) {
        const index = base64Chars.indexOf(input.charAt(i)); // 查找字符的位置
        binaryString += index.toString(2).padStart(6, '0'); // 将位置转换为 6 位二进制
    }

    // 每 8 位一个字节,转换为字节
    let decodedBytes = [];
    for (let i = 0; i < binaryString.length; i += 8) {
        const byte = binaryString.slice(i, i + 8);
        decodedBytes.push(parseInt(byte, 2)); // 将二进制转为字节
    }

    // 将字节数组转换为 UTF-8 字符串
    let decodedString = new TextDecoder().decode(new Uint8Array(decodedBytes));

    return decodedString;
}

// 示例
const decoded = base64Decode(encoded);
console.log('Decoded:', decoded);//Decoded: 你好润,Base64!
解释:
  • Base64 解码:首先将 Base64 字符串转换为 6 位二进制字符串。然后每 8 位恢复为一个字节。
  • 还原字符:通过 TextDecoder 将解码后的字节数组还原为 UTF-8 字符串。

3. 方法二:使用 TextEncoderTextDecoder API

如果不想手动实现 Base64 编码和解码的过程,现代浏览器提供了 TextEncoderTextDecoder API,可以帮助我们简化编码和解码过程。我们可以直接使用这些 API 来将中文字符串转换为字节数组,再进行 Base64 编码。

(1) 中文字符串的 Base64 编码
function base64EncodeUsingAPI(input) {
    // 使用 TextEncoder 将字符串转换为 UTF-8 字节
    const utf8Bytes = new TextEncoder().encode(input);

    // 将字节数组转换为 Base64 字符串
    return btoa(String.fromCharCode.apply(null, utf8Bytes));
}

// 示例
const input = '你好red润,Base64!';
const encoded = base64EncodeUsingAPI(input);
console.log('Encoded:', encoded);//Encoded: 5L2g5aW9cmVk5ram77yMQmFzZTY077yB
解释:
  • TextEncoder:通过 TextEncoder 将字符串转换为 UTF-8 字节数组。
  • btoabtoa 是一个内建的 JavaScript 函数,用于将二进制数据转换为 Base64 编码的字符串。需要注意的是,btoa 只能处理 8 位字符,因此我们通过 String.fromCharCode.apply 方法将字节数组转换为字符串,然后进行编码。
(2) 中文字符串的 Base64 解码
function base64DecodeUsingAPI(input) {
    // 使用 atob 解码 Base64 字符串
    const decodedData = atob(input);

    // 将解码后的字符串转换为字节数组
    const byteArray = new Uint8Array(decodedData.length);
    for (let i = 0; i < decodedData.length; i++) {
        byteArray[i] = decodedData.charCodeAt(i);
    }

    // 使用 TextDecoder 将字节数组转换为原始字符串
    return new TextDecoder().decode(byteArray);
}

// 示例
const decoded = base64DecodeUsingAPI(encoded);
console.log('Decoded:', decoded);// Decoded: 你好red润,Base64!
解释:
  • atobatob 函数用于解码 Base64 字符串。解码后的数据是一个原始的二进制字符串。
  • TextDecoder:通过 TextDecoder 将字节数组还原为 UTF-8 字符串。

4. 总结

在处理中文字符串的 Base64 编码与解码时,关键在于如何正确地将字符转换为字节数据。通过手动处理每个字节或使用现代的 TextEncoderTextDecoder API,我们可以确保中文字符能够正确地进行 Base64 编码与解码。

  • 手动实现:适用于那些需要详细控制编码过程的场景。你可以深入理解 Base64 编码的每个细节,并根据需要进行优化。
  • 使用 API:对于大多数情况,TextEncoderTextDecoder API 提供了一种更简洁、直接的方式来处理编码和解码,减少了手动操作的复杂性。
❌