高薪招聘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

1 回复

更多关于高薪招聘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,例如CompressionBlockCache大小和WriteBuffer管理,以适应信息流的时间序列或聊天消息的随机读写模式。
  • Raft:如果使用Hashicorp的Raft库实现分布式逻辑,需要深入理解日志复制、快照和成员变更的细节。一个常见的模式是将Raft用作一个可靠的配置或元数据存储,而非数据主路径。
  • CockroachDB/Postgres:在Go中,pgx驱动是高性能首选。需要熟练使用其连接池配置、批量操作(CopyFrom)、以及监听通知(Listen/Notify)功能。对于CockroachDB,要理解其分布式事务对应用模式的影响。

这个职位要求工程师能够将Go语言的并发模型、内存管理和性能特性(如减少指针逃逸、使用io.Reader/Writer接口进行零拷贝处理)与上述特定的存储引擎和分布式系统原理相结合,以构建和维护一个支撑十亿级用户的平台。代码的可观测性(Metrics, Tracing, Structured Logging)和自动化测试(包括集成测试和混沌测试)也是日常工作的基石。

回到顶部