第一章:Go后端架构演进的底层逻辑与全景认知
Go语言自诞生起便以“简洁、高效、并发友好”为设计信条,其底层运行时(runtime)、静态链接特性与原生goroutine调度模型,共同构成了后端架构演进的物理基础。不同于JVM或Node.js依赖复杂虚拟机层,Go二进制可直接运行于操作系统之上,消除了跨语言调用开销,使微服务粒度更细、部署更轻量——单个HTTP服务常以10MB以内二进制交付,启动耗时普遍低于50ms。
架构演进的核心驱动力
- 并发模型变革:从传统线程池(如Java ThreadPoolExecutor)转向M:N调度器,百万级goroutine可共存于单进程,天然适配高并发I/O密集场景;
- 依赖治理简化:
go mod强制语义化版本与不可变校验(sum.golang.org),避免“依赖地狱”,支撑模块化拆分与独立演进; - 可观测性内建支持:
net/http/pprof、expvar及runtime/trace无需第三方Agent即可采集CPU、内存、Goroutine阻塞等核心指标。
典型架构阶段对比
| 阶段 | 特征 | Go典型实践 |
|---|---|---|
| 单体服务 | 业务逻辑耦合,单进程部署 | http.ServeMux + database/sql |
| 模块化单体 | 逻辑分层,接口契约清晰 | internal/包隔离领域层,cmd/分离入口 |
| 云原生微服务 | 服务自治、弹性伸缩 | go-grpc + etcd服务发现 + OpenTelemetry链路追踪 |
快速验证并发模型差异
# 启动一个模拟高并发请求的Go服务(含pprof)
cat > server.go <<'EOF'
package main
import (
"net/http"
_ "net/http/pprof" // 自动注册/pprof路径
)
func handler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
EOF
go run server.go &
# 在另一终端触发10万并发请求并观察goroutine增长
ab -n 100000 -c 1000 http://localhost:8080/
# 访问 http://localhost:8080/debug/pprof/goroutine?debug=2 查看实时协程栈
该服务在ab压测下仅维持数百goroutine,远低于同等负载下pthread线程数,印证了Go调度器对资源的高效复用能力。
第二章:单体服务奠基期——高可用单进程架构实践
2.1 Go运行时调度模型与GMP在单体服务中的性能调优实践
Go 的 GMP 模型(Goroutine、M-thread、P-processor)是单体服务高并发能力的底层支柱。当服务 QPS 持续攀升时,需关注 P 数量与 CPU 核心的对齐、G 频繁阻塞导致的 M 阻塞扩散,以及 sysmon 对长时间运行 G 的抢占延迟。
关键调优手段
- 设置
GOMAXPROCS(runtime.NumCPU())确保 P 与物理核匹配 - 避免在 Goroutine 中执行无中断的密集计算(如
for {}) - 使用
runtime.Gosched()主动让出 P,或改用time.Sleep(0)触发协作式调度
GC 压力下的调度退化示例
// ❌ 危险:长循环阻塞 P,阻断其他 G 调度
for i := 0; i < 1e9; i++ {
_ = i * i // 无函数调用、无栈增长、无系统调用 → 不触发抢占
}
// ✅ 改进:插入调度点,允许 P 被抢占
for i := 0; i < 1e9; i++ {
if i%1000 == 0 {
runtime.Gosched() // 主动让出当前 P,供其他 G 运行
}
}
runtime.Gosched() 强制当前 G 让出 P,使其他就绪 G 可被调度;该调用开销极低(约 20ns),适用于可控粒度的计算分片场景。
P 与 M 绑定关系示意
graph TD
G1 -->|就绪| P1
G2 -->|就绪| P1
G3 -->|阻塞| M1
M1 -->|绑定| P1
M2 -->|空闲| P2
P1 -->|最大负载| 256G
| 指标 | 健康阈值 | 监控方式 |
|---|---|---|
golang_goroutines |
Prometheus + go_goroutines |
|
go_sched_goroutines_goroutines |
G/P > 200 → 调度积压 | runtime.ReadMemStats() |
2.2 标准库net/http深度定制:连接复用、超时控制与中间件链路治理
连接复用:Transport 的精细化配置
http.Transport 是复用的核心,需显式设置 MaxIdleConns 和 MaxIdleConnsPerHost 避免连接泄漏:
client := &http.Client{
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 30 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
},
}
MaxIdleConnsPerHost 控制单域名最大空闲连接数,防止 DNS 轮询场景下连接爆炸;IdleConnTimeout 决定空闲连接存活时长,过短增加握手开销,过长占用资源。
超时分层治理
| 超时类型 | 推荐值 | 影响范围 |
|---|---|---|
Timeout |
30s | 整个请求生命周期 |
IdleConnTimeout |
30s | 复用连接保活 |
TLSHandshakeTimeout |
5–10s | TLS 握手阶段 |
中间件链式编排
func MiddlewareChain(h http.Handler, mids ...func(http.Handler) http.Handler) http.Handler {
for i := len(mids) - 1; i >= 0; i-- {
h = mids[i](h)
}
return h
}
逆序组合确保外层中间件(如日志、鉴权)先执行,内层(如恢复 panic)后包裹——符合洋葱模型调用语义。
graph TD A[Client Request] –> B[Auth Middleware] B –> C[RateLimit Middleware] C –> D[Handler] D –> E[Recovery Middleware] E –> F[Response]
2.3 基于Go Module与Go Work的依赖分层管理与构建可重现性保障
Go 1.18 引入的 go work(工作区)机制,与传统 go mod 协同构成双层依赖治理模型:模块级隔离 + 工作区级聚合。
分层职责划分
go.mod:定义单模块语义版本、直接依赖及replace/exclude规则go.work:跨模块复用本地路径、统一use多个模块、屏蔽间接依赖冲突
可重现性关键实践
# go.work 示例(根目录)
go 1.22
use (
./backend
./frontend
./shared
)
此配置强制所有子模块共享同一份
go.sum校验和快照,并禁用远程代理缓存,确保go build在任意环境产出完全一致的二进制哈希。
构建一致性验证流程
graph TD
A[go.work 加载] --> B[解析所有 use 模块]
B --> C[合并 go.sum 并锁定 checksum]
C --> D[启用 -mod=readonly 阻止隐式修改]
D --> E[输出 reproducible binary]
| 维度 | go.mod 单模块 | go.work 多模块 |
|---|---|---|
| 依赖可见性 | 仅当前模块 | 全局统一视图 |
| 版本仲裁粒度 | 模块内 | 工作区级 |
go.sum 管理 |
独立维护 | 合并唯一校验源 |
2.4 单体服务可观测性基建:OpenTelemetry SDK集成与轻量级Metrics/Tracing落地
OpenTelemetry Java SDK 快速接入
在 pom.xml 中引入核心依赖:
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk</artifactId>
<version>1.39.0</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp-metrics</artifactId>
<version>1.39.0</version>
</dependency>
→ 该组合提供内存内指标采集能力与 OTLP 协议导出支持,1.39.0 版本已默认启用异步批处理,降低性能开销。
轻量级 Tracing 初始化
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
.addSpanProcessor(BatchSpanProcessor.builder(
OtlpGrpcSpanExporter.builder()
.setEndpoint("http://localhost:4317") // Collector 地址
.setTimeout(5, TimeUnit.SECONDS)
.build())
.build())
.build();
→ BatchSpanProcessor 缓存并批量推送 Span,setTimeout 控制最大等待时长,避免阻塞业务线程。
Metrics 采集关键维度对比
| 指标类型 | 示例标签 | 推荐采样策略 | 存储开销 |
|---|---|---|---|
| Counter | method=GET, status=200 |
全量记录 | 低 |
| Histogram | http.server.duration |
分位数聚合 | 中 |
数据流向简图
graph TD
A[业务代码] --> B[OTel SDK]
B --> C[内存缓冲区]
C --> D{Batch 触发?}
D -->|是| E[OTLP gRPC 导出]
D -->|否| C
2.5 灰度发布与配置热加载:基于fsnotify+Viper的零停机配置动态生效方案
传统配置更新需重启服务,而灰度发布要求配置按批次、按标签动态生效。我们结合 fsnotify 监听文件系统变更,配合 Viper 的 WatchConfig() 机制,实现毫秒级热重载。
核心监听逻辑
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
log.Printf("Config changed: %s, action: %s", e.Name, e.Op)
// 触发灰度策略校验(如 version=1.2.0-beta)
})
该回调在配置文件 config.yaml 被 echo "feature.flag: true" >> config.yaml 修改时立即触发;e.Op 包含 fsnotify.Write,是唯一可靠变更信号。
灰度路由匹配规则
| 环境标签 | 配置键路径 | 生效比例 |
|---|---|---|
| staging | features.auth |
100% |
| prod-v2 | features.new-ui |
30% |
配置热加载流程
graph TD
A[fsnotify 检测文件写入] --> B{Viper 解析新内容}
B --> C[校验 schema 合法性]
C --> D[按 label 匹配灰度组]
D --> E[原子替换 runtime config]
第三章:微服务拆分期——领域驱动与通信契约演进
3.1 DDD战术建模在Go工程中的映射:Value Object、Aggregate与Repository接口设计实践
DDD战术建模在Go中需契合其结构化与接口驱动特性。Value Object强调不可变性与值语义,Aggregate保障一致性边界,Repository则抽象持久化细节。
Value Object:邮箱类型安全封装
type Email struct {
value string
}
func NewEmail(s string) (*Email, error) {
if !isValidEmail(s) {
return nil, errors.New("invalid email format")
}
return &Email{value: strings.ToLower(s)}, nil // 不可变 + 规范化
}
NewEmail校验并归一化输入;返回指针避免意外拷贝;value字段私有确保封装性。
Aggregate根与Repository契约
type OrderID string
type Order struct {
ID OrderID
Items []OrderItem // 值对象集合
Status OrderStatus
version uint64
}
type OrderRepository interface {
Save(ctx context.Context, o *Order) error
ByID(ctx context.Context, id OrderID) (*Order, error)
}
Order为聚合根,含内部状态与行为;OrderRepository仅声明契约,解耦实现(如GORM或Redis)。
| 组件 | Go实现要点 | DDD语义 |
|---|---|---|
| Value Object | 不可变、无ID、重写Equal/Hash | 相等性基于值而非引用 |
| Aggregate | 根ID唯一、内部实体受保护 | 一致性边界 |
| Repository | 接口定义、不暴露SQL/ORM细节 | 持久化透明性 |
graph TD
A[Client] --> B[OrderService]
B --> C[OrderRepository]
C --> D[(Database)]
C --> E[(Cache)]
style C fill:#4a6fa5,stroke:#333
3.2 gRPC-Gateway双协议网关架构:Protobuf IDL驱动的前后端契约一致性保障
gRPC-Gateway 在单份 .proto 文件基础上,自动生成 gRPC 服务端与 REST/JSON API 网关,实现「一处定义、两端消费」。
核心机制
- Protobuf IDL 作为唯一契约源,约束字段类型、必选性、验证规则(如
google.api.field_behavior) protoc插件链:grpc-go+grpc-gateway同步生成 Go 接口与 HTTP 路由映射- JSON 编解码自动适配 protobuf 的
json_name与google.api.http注解
示例:HTTP 路由声明
service UserService {
rpc GetUser(GetUserRequest) returns (GetUserResponse) {
option (google.api.http) = {
get: "/v1/users/{id}"
additional_bindings { post: "/v1/users:lookup" body: "*" }
};
}
}
逻辑分析:
get: "/v1/users/{id}"将路径参数id自动绑定至GetUserRequest.id字段;body: "*"表示 POST 请求体完整映射为请求消息。google.api.http是 gRPC-Gateway 解析的元数据扩展,无需额外配置即可生成反向代理路由。
协议转换流程
graph TD
A[REST Client] -->|JSON over HTTP| B(gRPC-Gateway)
B -->|ProtoBuf over gRPC| C[gRPC Server]
C -->|ProtoBuf| B
B -->|JSON| A
3.3 服务间强一致性通信:gRPC流控、重试策略与自适应负载均衡(xDS兼容实现)
在微服务强一致性场景下,gRPC需兼顾低延迟、高可靠与动态拓扑适配。核心依赖三重协同机制:
流控与背压传导
gRPC原生支持WindowUpdate与MAX_CONCURRENT_STREAMS,但需结合应用层语义增强。例如在订单状态同步服务中启用Per-RPC flow control:
// service.proto
service OrderSync {
rpc SyncStream(stream OrderEvent) returns (stream SyncAck) {
option (google.api.http) = {
post: "/v1/orders/sync"
body: "*"
};
}
}
该定义启用双向流式通信,配合grpc.MaxConcurrentStreams(100)参数可防止下游过载;SyncAck携带retry_hint_ms字段,驱动客户端动态调整发送节奏。
自适应负载均衡(xDS兼容)
通过Envoy xDS v3 API动态下发端点权重与健康阈值:
| Cluster | Load Assignment | Policy |
|---|---|---|
| order-svc | eds_cluster |
LEAST_REQUEST + PANIC_THRESHOLD=50% |
重试策略配置
采用指数退避+语义化重试判定:
# envoy.yaml snippet
retry_policy:
retry_on: "5xx,connect-failure,refused-stream"
num_retries: 3
retry_back_off:
base_interval: 0.1s
max_interval: 2s
重试仅在UNAVAILABLE或DEADLINE_EXCEEDED且非幂等写操作时触发,避免状态重复提交。
第四章:云原生规模化期——弹性、韧性与平台化治理
4.1 Kubernetes Operator模式在Go中的实现:自定义资源CRD与控制器Reconcile循环优化
Operator核心在于将运维逻辑编码为控制器,通过监听自定义资源(CR)变化驱动状态收敛。
CRD定义要点
spec描述期望状态(如副本数、镜像版本)status由控制器更新,反映实际运行态- 必须启用
subresources.status支持状态独立更新
Reconcile循环优化策略
func (r *AppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
var app myv1.App
if err := r.Get(ctx, req.NamespacedName, &app); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// 仅当spec变更或status未同步时触发处理
if !reflect.DeepEqual(app.Spec, app.Status.ObservedGeneration) {
// ... reconcile logic
app.Status.ObservedGeneration = app.Generation
r.Status().Update(ctx, &app) // 原子更新status子资源
}
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
}
逻辑分析:
ObservedGeneration与Generation对齐可避免重复执行;Status().Update()避免全量对象写入,降低apiserver压力。RequeueAfter替代高频轮询,提升伸缩性。
| 优化维度 | 传统做法 | 推荐实践 |
|---|---|---|
| 状态更新 | 全量对象PATCH | status 子资源独立UPDATE |
| 触发条件 | 每次事件无差别处理 | 对比Generation与ObservedGeneration |
| 重试机制 | 固定短间隔轮询 | 动态RequeueAfter + 错误退避 |
graph TD
A[CR事件到达] --> B{Generation == ObservedGeneration?}
B -->|是| C[跳过Reconcile]
B -->|否| D[执行业务逻辑]
D --> E[更新Status.ObservedGeneration]
E --> F[Status.Update]
4.2 分布式事务终局方案:Saga模式在Go微服务中的状态机编排与补偿日志持久化实践
Saga 模式通过长活事务拆解为本地事务序列 + 显式补偿,规避两阶段锁开销,适用于高并发、跨服务的最终一致性场景。
状态机驱动的执行流程
type SagaState struct {
OrderID string `json:"order_id"`
State string `json:"state"` // "created" → "paid" → "shipped" → "completed"
Compensated []string `json:"compensated"` // 已执行的补偿步骤
}
// 状态跃迁需原子更新 + 日志落盘
func (s *SagaManager) Transition(ctx context.Context, orderID, from, to string) error {
return s.db.Transaction(ctx, func(tx *sql.Tx) error {
// 1. 检查当前状态合法性(防止跳步)
// 2. 更新状态字段
// 3. 写入 saga_log 表(含 step、timestamp、payload)
return s.logRepo.Append(ctx, tx, SagaLog{
OrderID: orderID, Step: to, Timestamp: time.Now(),
Payload: map[string]any{"from": from},
})
})
}
逻辑说明:Transition 封装状态校验、DB 更新与日志写入三步原子操作;SagaLog 表需包含 order_id(索引)、step(枚举)、seq_no(防重放)和 status(pending/committed/compensated)。
补偿日志持久化关键字段设计
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | BIGINT PK | 全局唯一日志ID |
| order_id | VARCHAR | 关联业务单据,支持查询 |
| step | VARCHAR | 步骤名(如 “reserve_stock”) |
| comp_handler | TEXT | 补偿接口URL或函数名 |
| status | ENUM | pending / executed / failed |
故障恢复流程
graph TD
A[检测超时/失败事件] --> B{查询 saga_log }
B --> C[按 seq_no 倒序扫描]
C --> D[定位首个未补偿步骤]
D --> E[调用对应 comp_handler]
E --> F[更新 log.status = compensated]
4.3 多租户隔离架构:基于Go泛型+Context Value的租户上下文透传与数据分片路由
租户上下文注入与提取
使用 context.WithValue 注入租户标识,并通过泛型封装类型安全的访问器:
type TenantID string
func WithTenant(ctx context.Context, tid TenantID) context.Context {
return context.WithValue(ctx, tenantKey{}, tid)
}
func TenantFrom(ctx context.Context) (TenantID, bool) {
v := ctx.Value(tenantKey{})
tid, ok := v.(TenantID)
return tid, ok
}
tenantKey{} 是空结构体,避免全局 key 冲突;泛型未显式出现但为后续 TenantContext[T any] 扩展预留接口契约。
数据路由策略映射
租户 ID 经哈希后映射至分片节点:
| 租户ID | Hash(Mod16) | 分片节点 |
|---|---|---|
acme-001 |
5 | shard-5 |
beta-002 |
12 | shard-12 |
路由执行流程
graph TD
A[HTTP Request] --> B[Middleware: 解析X-Tenant-ID]
B --> C[WithTenant ctx]
C --> D[Handler: TenantFrom ctx]
D --> E[ShardRouter.Route(tid)]
E --> F[DB Query on shard-N]
4.4 Serverless化演进:Cloudflare Workers/WASM-Go与Knative Serving的混合部署策略
混合架构需按流量特征分层调度:边缘轻量逻辑交由 Cloudflare Workers 执行,WASM-Go 编译为零依赖二进制;核心有状态服务保留在 Knative Serving 的 Kubernetes 集群中。
流量路由决策模型
graph TD
A[HTTP 请求] --> B{路径匹配 /api/edge}
B -->|是| C[Cloudflare Workers]
B -->|否| D[Knative Serving via Istio Gateway]
WASM-Go 边缘函数示例
// main.go —— 编译为 wasm32-wasi 目标
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok", "edge": "cf"})
})
}
使用
tinygo build -o main.wasm -target wasm32-wasi main.go构建;wasm32-wasi目标确保无 OS 依赖,适配 Workers 的 WASI 运行时;Content-Type显式设置避免 MIME 推断失败。
混合部署能力对比
| 维度 | Cloudflare Workers + WASM-Go | Knative Serving |
|---|---|---|
| 冷启动延迟 | 100–500ms | |
| 并发模型 | 事件驱动、无状态 | Pod 级弹性伸缩 |
| 本地存储支持 | ❌(仅 KV/Cache) | ✅(Volume/PVC) |
该策略兼顾边缘实时性与后端可维护性。
第五章:面向千万级DAU的架构终态与未来范式
极致弹性调度:Kubernetes + eBPF 的混合资源编排
在字节跳动抖音核心推荐服务中,DAU峰值突破1.2亿时,传统HPA基于CPU/Memory指标的扩缩容延迟高达90秒。团队落地了基于eBPF实时采集P99延迟、QPS突增率、GPU显存碎片率的多维信号融合控制器,结合Kubernetes的Custom Metrics Adapter与Vertical Pod Autoscaler v2,将冷启扩容时间压缩至14秒内。关键改造包括:在Node上部署eBPF程序latency_tracer.o捕获gRPC请求链路耗时,通过metrics-server暴露为custom.metrics.k8s.io/v1beta1 API供HPA消费。以下为真实生效的HPA配置片段:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
spec:
metrics:
- type: External
external:
metric:
name: grpc_p99_latency_ms
selector: {matchLabels: {service: "rec-retrieval"}}
target:
type: AverageValue
averageValue: "150m"
服务网格零信任化:mTLS+SPIFFE身份联邦实践
美团外卖订单中心在2023年完成全链路Service Mesh迁移后,日均处理4.7亿次跨域调用。其安全架构摒弃传统IP白名单,采用SPIFFE ID(spiffe://meituan.com/order/checkout-v2)作为服务唯一身份标识,所有Envoy Sidecar强制启用双向mTLS,并通过自研SPIRE Agent实现与内部PKI CA的自动轮换。当某次灰度发布引发checkout-service证书吊销时,Mesh控制面在3.2秒内完成全集群证书刷新,未产生单笔订单丢失。
| 组件 | 版本 | 部署密度(每节点) | SLA保障机制 |
|---|---|---|---|
| Envoy Proxy | v1.26.2 | 1 | 自动熔断+健康检查重试 |
| SPIRE Agent | v1.7.0 | 1 | 本地缓存+CA心跳保活 |
| Istiod | v1.18.3 | 集群级1实例 | 多可用区主备切换 |
混沌工程常态化:基于流量染色的故障注入平台
快手直播中台构建了ChaosFlow平台,支持按用户UID哈希值精准注入故障。例如对UID末3位为[137, 482, 901]的千万级用户,在CDN层注入500ms网络抖动,同时在应用层模拟Redis Cluster分片不可用——仅影响live_comment_cache命名空间,不影响live_user_profile。该平台每日执行27类故障场景,2024年Q1提前发现3起潜在雪崩风险,包括:评论服务因redis-bloom模块OOM导致连接池耗尽、直播流ID生成器在时钟回拨时重复发号等。
存储计算分离:TiDB HTAP集群的实时分析演进
小红书笔记搜索业务采用TiDB 7.5构建统一HTAP架构,将原MySQL分库分表+Spark离线数仓架构替换为单集群承载OLTP与OLAP。关键优化包括:
- 启用
TiFlash列式副本,对note_index表建立二级索引idx_user_id_created_at并设置REPLICATION 3; - 使用
ALTER TABLE ... SET TIFLASH REPLICA 3命令动态调整热点表副本分布; - 在TiDB Dashboard中配置
slow_query_threshold=50ms,自动捕获长事务并推送至飞书告警群。
边缘智能协同:CDN节点嵌入轻量推理引擎
B站番剧推荐在2000+边缘CDN节点(阿里云ECI+腾讯云ECM)部署TensorRT Lite推理服务,将user_embedding向量生成从中心机房下沉至边缘。实测显示:用户打开番剧页时,CDN节点利用本地缓存的user_id → embedding映射表(TTL=30min),结合实时行为特征(最近点击/停留时长)生成个性化排序向量,端到端延迟从820ms降至190ms,首屏加载成功率提升至99.992%。
无服务器化演进:事件驱动架构下的函数编排
得物App商品详情页重构中,将原单体Java服务拆解为17个Serverless函数,通过EventBridge构建有向无环图(DAG)。例如“查看商品”事件触发链:fetch_sku_info → check_stock(并行调用warehouse_api和logistics_api) → generate_recommend → render_html。使用AWS Step Functions状态机定义流程,每个函数最大内存设为3GB,冷启动平均耗时控制在320ms以内,支撑大促期间每秒23万次事件处理。
graph LR
A[用户请求] --> B(fetch_sku_info)
B --> C{库存检查}
C --> D[warehouse_api]
C --> E[logistics_api]
D & E --> F[generate_recommend]
F --> G[render_html]
G --> H[返回HTML] 