
Go 在线教育平台架构直播、录播和题库服务的微服务拆分一、单体架构扛不住万人直播该拆了典型的在线教育平台起步时都是一个单体应用用户认证、课程管理、直播推流、录播回放、题库练习全部塞在一个 war 包或一个 go binary 里。初期几百个用户一切正常但当直播课突破 1000 人同时在线时推流模块的 CPU 飙升顺带拖垮了用户认证服务——所有人都登录不了包括正在上课的学生。爆发点通常出现在三个场景的叠加晚间黄金时段万人直播推流高带宽 高并发、考试周题库服务的瞬时 QPS 飙升学生集中刷题、以及录播回放的视频转码任务堆积。这些场景对资源的需求完全不同——直播需要稳定的网络带宽题库需要低延迟的数据库响应转码需要大量的 CPU。单体架构无法为不同场景做差异化资源分配。二、微服务拆分策略按业务域和变更频率双维度切分拆分不能盲目——微服务太多会引入分布式复杂度太少则达不到隔离效果拆分遵循两个原则第一按业务域直播、录播、题库是三个不同的业务域它们的领域语言和变更频率完全不同。第二按变更频率弹幕服务变化快需要独立部署不能因为改一个弹幕效果就重上整个平台。三、Go 服务间通信的实现package edu import ( context encoding/json fmt net/http time ) // ServiceRegistry 服务注册与发现 type ServiceRegistry struct { services map[string]string // serviceName - baseURL } func NewServiceRegistry() *ServiceRegistry { return ServiceRegistry{services: make(map[string]string)} } func (sr *ServiceRegistry) Register(name, baseURL string) { sr.services[name] baseURL } // EducationClient 教育平台客户端聚合各微服务调用 type EducationClient struct { registry *ServiceRegistry httpClient *http.Client } // NewEducationClient 创建客户端 func NewEducationClient(registry *ServiceRegistry) *EducationClient { return EducationClient{ registry: registry, httpClient: http.Client{ Timeout: 5 * time.Second, Transport: http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 20, IdleConnTimeout: 90 * time.Second, }, }, } } // CourseInfo 课程信息聚合需要跨服务查询 type CourseInfo struct { Course *CourseDetail json:course LiveRoom *LiveRoomInfo json:live_room,omitempty Exercises []*Exercise json:exercises,omitempty } // GetAggregatedCourseInfo 聚合课程、直播、题库信息 func (c *EducationClient) GetAggregatedCourseInfo( ctx context.Context, courseID string, ) (*CourseInfo, error) { result : CourseInfo{} // 并行查询多个服务使用 errgroup 控制 errCh : make(chan error, 3) doneCh : make(chan struct{}, 3) // 查询课程基本信息 go func() { course, err : c.getCourse(ctx, courseID) if err ! nil { errCh - fmt.Errorf(课程服务查询失败: %w, err) return } result.Course course doneCh - struct{}{} }() // 查询直播间状态 go func() { room, err : c.getLiveRoom(ctx, courseID) if err ! nil { // 直播间可能不存在纯录播课非致命错误 errCh - nil doneCh - struct{}{} return } result.LiveRoom room doneCh - struct{}{} }() // 查询关联习题 go func() { exercises, err : c.getExercises(ctx, courseID) if err ! nil { errCh - fmt.Errorf(题库服务查询失败: %w, err) return } result.Exercises exercises doneCh - struct{}{} }() // 等待所有查询完成或失败 completed : 0 for completed 3 { select { case err : -errCh: if err ! nil { return nil, err } completed case -doneCh: completed case -ctx.Done(): return nil, ctx.Err() } } return result, nil } func (c *EducationClient) getCourse( ctx context.Context, courseID string, ) (*CourseDetail, error) { baseURL, ok : c.registry.services[course-service] if !ok { return nil, fmt.Errorf(课程服务未注册) } url : fmt.Sprintf(%s/api/v1/courses/%s, baseURL, courseID) return c.doGet(ctx, url, CourseDetail{}) } func (c *EducationClient) getLiveRoom( ctx context.Context, courseID string, ) (*LiveRoomInfo, error) { baseURL : c.registry.services[live-service] url : fmt.Sprintf(%s/api/v1/rooms?course_id%s, baseURL, courseID) return c.doGet(ctx, url, LiveRoomInfo{}) } func (c *EducationClient) getExercises( ctx context.Context, courseID string, ) ([]*Exercise, error) { baseURL : c.registry.services[exercise-service] url : fmt.Sprintf(%s/api/v1/exercises?course_id%s, baseURL, courseID) return c.doGet(ctx, url, []*Exercise{}) } func (c *EducationClient) doGet( ctx context.Context, url string, result interface{}, ) (interface{}, error) { req, err : http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err ! nil { return nil, fmt.Errorf(创建请求失败: %w, err) } resp, err : c.httpClient.Do(req) if err ! nil { return nil, fmt.Errorf(HTTP 请求失败: %w, err) } defer resp.Body.Close() if resp.StatusCode 400 { return nil, fmt.Errorf(服务返回异常状态码 %d, resp.StatusCode) } if err : json.NewDecoder(resp.Body).Decode(result); err ! nil { return nil, fmt.Errorf(JSON 解析失败: %w, err) } return result, nil } // CourseDetail 课程详情 type CourseDetail struct { ID string json:id Title string json:title } // LiveRoomInfo 直播间信息 type LiveRoomInfo struct { RoomID string json:room_id Status string json:status // live, ended OnlineNum int json:online_num } // Exercise 习题 type Exercise struct { ID string json:id Title string json:title }四、边界分析与 Trade-offs数据一致性的取舍微服务意味着分布式数据。课程表和题库之间如果存在数据依赖如练习题关联课件不能用数据库事务保证一致性。可选方案Saga 模式补偿事务或用消息队列实现最终一致性。教育场景对实时一致性要求不高定价课更新和题库更新之间差几分钟可以接受最终一致性是更好的选择。服务粒度的平衡拆解得太细会带来服务雪崩——一个页面需要调用 15 个微服务任何一个超时都导致页面失败。解决方式是 BFFBackend For Frontend聚合层在服务端完成多服务聚合前端只需一次请求。但 BFF 本身也可以成为单点故障——需要做好超时控制和熔断。直播服务的特殊隔离需求直播推流服务应该部署在独立的物理集群上最好靠近 CDN 节点与其他业务服务做物理隔离。因为直播对网络带宽的占用是独占式的不能用容器化环境的网络 QoS 来保证。题库服务的热点数据考试周某些热门试题的 QPS 可能达到平时的 100 倍。本地缓存如 BigCache比集中式缓存Redis更适合这种场景——试题内容不常变可以做本地缓存过期时间设为 1 小时降低 Redis 压力。五、总结在线教育平台的微服务拆分核心是按照业务域和变更频率两个维度交叉切分。直播、录播、题库是三个资源特征完全不同的服务必须独立部署和弹性伸缩。服务间通信用 HTTP JSON 足够满足教育场景的延迟要求100ms 以内不需要上 gRPC 增加运维复杂度。最容易被忽略的是 BFF 聚合层的超时策略——查询课程是核心路径超时 2 秒查询直播状态是非核心路径超时 500ms宁可降级也不等。