高薪招聘Golang高级后端工程师(90-125万欧元/年)远程/阿姆斯特丹
高薪招聘Golang高级后端工程师(90-125万欧元/年)远程/阿姆斯特丹 *公司:https://getstream.io/ *基本薪资:90,000 欧元 - 125,000 欧元(取决于地点和经验) *职位:高级后端工程师 *办公政策:目前位于欧盟或英国可远程办公。阿姆斯特丹为混合办公(可提供搬迁支持) *地点:欧盟/英国或荷兰 *经验级别:高级 *技术栈:Golang *简要描述:
Stream 是一个用于构建动态信息流、聊天和实时视频的 API。我们为数千个应用程序提供此服务,拥有超过 10 亿终端用户。大约 4-5 年前,我们从 Python 切换到了 Go,现在除了机器学习部分外,所有系统都运行在 Go、RocksDB、Raft、Redis 和 CockroachDB/Postgres 上。
一些亮点:
- 高流量环境,专注于可扩展性
- 3 种不同的产品,每种都有其独特的挑战。
- 动态信息流对存储要求非常高,采用了读扩散 + 写扩散的实现方式。
- 视频需要高效的 Go 代码,以确保低 CPU 使用率和低延迟。
- 聊天需要对数据库和数据反规范化有深入理解,聊天中的某些部分(如未读计数)对存储要求很高。
- 目前我们正在招聘欧盟远程以及阿姆斯特丹办公室的高级工程师。
- 来自不同语言/背景的候选人也会被考虑。即使您只有一点 Go 经验,但多年来一直是后端高级工程师或更高级别,也是可以的。
在 Stream,我们的客户是工程师和产品负责人,因此您将加入一家由工程驱动的公司。
欢迎随时申请,如有任何问题,请随时联系! 🙂
更多关于高薪招聘Golang高级后端工程师(90-125万欧元/年)远程/阿姆斯特丹的实战教程也可以访问 https://www.itying.com/category-94-b0.html
更多关于高薪招聘Golang高级后端工程师(90-125万欧元/年)远程/阿姆斯特丹的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
这是一个非常吸引人的高级职位,Stream的技术栈和业务场景对Golang工程师来说极具挑战性和成长空间。从Python全面转向Go,并在高流量、低延迟的实时通信领域深度应用,这要求工程师不仅精通语言特性,更要深刻理解系统设计。
针对职位描述中提到的三个产品方向,这里有一些Go实现的代码示例和关键点,它们直接关联到你们提到的技术挑战:
1. 动态信息流(读扩散+写扩散)
混合模式(Hybrid Feed)是平衡读写压力的关键。以下是一个简化的扇出(Fan-out)写扩散逻辑示例,展示了如何异步处理用户新帖子到其粉丝时间线:
package main
import (
"context"
"sync"
"time"
)
type FeedService struct {
timelineRepo TimelineRepository
socialGraph SocialGraphService
workerPool chan struct{}
}
// 用户发布新活动
func (s *FeedService) PostActivity(ctx context.Context, actor string, activity Activity) error {
// 1. 写入作者的个人流(写扩散源)
if err := s.writeToPersonalFeed(ctx, actor, activity); err != nil {
return err
}
// 2. 异步扇出到粉丝的时间线(写扩散)
go s.fanOutToFollowers(ctx, actor, activity)
return nil
}
// 异步扇出到粉丝
func (s *FeedService) fanOutToFollowers(ctx context.Context, actor string, activity Activity) {
followers, err := s.socialGraph.GetFollowers(ctx, actor)
if err != nil {
// 处理错误,可能进入重试队列
return
}
var wg sync.WaitGroup
// 使用工作池控制并发,防止粉丝过多时资源耗尽
for _, follower := range followers {
s.workerPool <- struct{}{} // 获取令牌
wg.Add(1)
go func(followerID string) {
defer wg.Done()
defer func() { <-s.workerPool }() // 释放令牌
// 写入每个粉丝的“时间线”聚合流
// 这里通常使用批量插入优化,示例为单次操作
s.timelineRepo.AddToTimeline(ctx, followerID, activity)
}(follower)
}
wg.Wait()
}
// 用户读取自己的时间线(读扩散源)
func (s *FeedService) GetTimeline(ctx context.Context, user string, limit int) ([]Activity, error) {
// 直接从用户的时间线聚合流中读取(写扩散的结果)
return s.timelineRepo.GetTimeline(ctx, user, limit)
}
关键点:在实际系统中,fanOutToFollowers 会与消息队列(如Kafka)结合,将粉丝列表分片后由多个消费者并行处理,并采用批量写入(如使用pgx.CopyFrom写入PostgreSQL或批量写入RocksDB)来极大提升吞吐量。对于大V用户,可能会降级为纯读扩散(拉模式)。
2. 视频服务(低CPU与低延迟)
视频信令或元数据处理需要极致的效率。以下示例展示了如何利用sync.Pool减少GC压力,以及使用context实现毫秒级超时控制,这对维持低延迟至关重要:
package main
import (
"context"
"encoding/json"
"net/http"
"sync"
"time"
)
var metadataPool = sync.Pool{
New: func() interface{} {
return &VideoMetadata{
// 预分配切片,避免后续增长
Tags: make([]string, 0, 5),
}
},
}
type VideoMetadata struct {
ID string `json:"id"`
Timestamp time.Time `json:"ts"`
Tags []string `json:"tags"`
// ... 其他字段
}
// 处理视频信令请求,要求P99延迟 < 50ms
func (h *VideoHandler) HandleSignal(w http.ResponseWriter, r *http.Request) {
// 设置严格的超时上下文
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Millisecond)
defer cancel()
// 从池中获取对象,减少GC
meta := metadataPool.Get().(*VideoMetadata)
defer func() {
// 使用后重置并放回池中
meta.ID = ""
meta.Tags = meta.Tags[:0]
metadataPool.Put(meta)
}()
// 解析请求(使用高性能JSON解析器,如json-iterator)
if err := json.NewDecoder(r.Body).Decode(meta); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// 关键路径处理:例如,将信令路由到正确的SFU节点
resultChan := make(chan *RoutingResult, 1)
go h.routeSignal(ctx, meta, resultChan)
select {
case <-ctx.Done():
// 超时或取消,快速失败,避免阻塞
http.Error(w, "processing timeout", http.StatusGatewayTimeout)
return
case result := <-resultChan:
// 成功响应
json.NewEncoder(w).Encode(result)
}
}
func (h *VideoHandler) routeSignal(ctx context.Context, meta *VideoMetadata, result chan<- *RoutingResult) {
// 模拟一个需要低延迟完成的计算或查询
// 例如:基于一致性哈希查找SFU节点
// 这里必须是非阻塞且快速的
select {
case <-ctx.Done():
return
default:
// 执行路由逻辑...
result <- &RoutingResult{Node: "sfu-eu-1"}
}
}
关键点:除了对象池,在视频流处理中会大量使用无锁数据结构、内存映射文件(处理视频片段)以及精心设计的goroutine生命周期管理,防止goroutine泄漏。CPU Profiling (pprof) 是日常工具,用于定位热点。
3. 聊天服务(数据反规范化与存储优化)
未读计数是典型的读写密集型数据,需要反规范化设计。以下示例展示了如何利用Redis原子操作和数据库的监听机制(如CockroachDB的CHANGEFEED或PostgreSQL的LISTEN/NOTIFY)来维护实时未读计数:
package main
import (
"context"
"fmt"
"github.com/go-redis/redis/v8"
"github.com/jackc/pgx/v4"
)
type UnreadCounter struct {
redisClient *redis.Client
dbConn *pgx.Conn
}
// 发送消息时更新未读计数
func (uc *UnreadCounter) OnMessageSent(ctx context.Context, channelID, senderID string, recipientIDs []string) error {
pipe := uc.redisClient.Pipeline()
// 为每个接收者(除了发送者自己)增加未读计数
for _, recipientID := range recipientIDs {
if recipientID == senderID {
continue // 不给自己增加未读
}
key := fmt.Sprintf("unread:%s:%s", recipientID, channelID)
// 使用原子递增,并设置过期时间防止数据无限增长
pipe.Incr(ctx, key)
pipe.Expire(ctx, key, 30*24*time.Hour) // 例如保留30天
}
_, err := pipe.Exec(ctx)
return err
}
// 用户读取某个频道消息后,重置未读计数
func (uc *UnreadCounter) MarkAsRead(ctx context.Context, userID, channelID string) error {
key := fmt.Sprintf("unread:%s:%s", userID, channelID)
// 原子性地删除计数
_, err := uc.redisClient.Del(ctx, key).Result()
if err != nil {
return err
}
// 可选:将“已读位置”持久化到CockroachDB/Postgres,用于历史同步或备份
_, err = uc.dbConn.Exec(ctx,
`INSERT INTO user_read_positions (user_id, channel_id, last_read_at)
VALUES ($1, $2, NOW())
ON CONFLICT (user_id, channel_id) DO UPDATE SET last_read_at = NOW()`,
userID, channelID)
return err
}
// 监听数据库的“消息”表变更,确保缓存与源数据最终一致
func (uc *UnreadCounter) startChangeListener(ctx context.Context) {
// 使用CockroachDB的CHANGEFEED或PG的逻辑解码
// 这是一个简化示例,实际会使用CDC工具
rows, _ := uc.dbConn.Query(ctx, "EXPERIMENTAL CHANGEFEED FOR messages")
for rows.Next() {
var change struct {
Table string
After []byte // JSON格式的新行数据
}
rows.Scan(&change.Table, &change.After)
// 解析change.After,更新或清理对应的Redis未读计数
// 这处理了通过其他途径(如控制台直接操作DB)导致的数据不一致
}
}
关键点:反规范化设计是核心。除了未读计数,聊天消息本身可能也会冗余存储(例如,在channel_messages表之外,为每个用户存储一份user_messages副本以加速“我的消息”查询)。数据一致性通过CDC监听、定期修复任务或事务性发件箱模式(Transactional Outbox)来保证。
通用技术栈深入点
- RocksDB:在Go中通常通过CGO绑定(如
gorocksdb)使用。需要重点调优Options,例如Compression、BlockCache大小和WriteBuffer管理,以适应信息流的时间序列或聊天消息的随机读写模式。 - Raft:如果使用Hashicorp的Raft库实现分布式逻辑,需要深入理解日志复制、快照和成员变更的细节。一个常见的模式是将Raft用作一个可靠的配置或元数据存储,而非数据主路径。
- CockroachDB/Postgres:在Go中,
pgx驱动是高性能首选。需要熟练使用其连接池配置、批量操作(CopyFrom)、以及监听通知(Listen/Notify)功能。对于CockroachDB,要理解其分布式事务对应用模式的影响。
这个职位要求工程师能够将Go语言的并发模型、内存管理和性能特性(如减少指针逃逸、使用io.Reader/Writer接口进行零拷贝处理)与上述特定的存储引擎和分布式系统原理相结合,以构建和维护一个支撑十亿级用户的平台。代码的可观测性(Metrics, Tracing, Structured Logging)和自动化测试(包括集成测试和混沌测试)也是日常工作的基石。

