阿里 P7 三面,kafka Borker 日志持久化没答上

👏作者简介:大家好,我是爱敲代码的小黄,阿里巴巴淘天Java开发工程师

📕系列专栏:Spring源码、Netty源码、Kafka源码、JUC源码、dubbo源码系列

🔥如果感觉博主的文章还不错的话,请👍三连支持👍一下博主哦

🍂博主正在努力完成2023计划中:以梦为马,扬帆起航,2023追梦人

阿里 P7 三面凉凉,kafka Borker 日志持久化没答上来

一、引言

前段时间有个朋友,去面了阿里集团的P7岗位,很遗憾的是三面没有过

其中有一个 kafkaBorker 日志如何持久化的问题没有答上来

今天正好写一篇源码文章给朋友复盘一下

虽然现在是互联网寒冬,但乾坤未定,你我皆是黑马!

废话不多说,发车!

二、日志原理介绍

在讲 Kafka 日志源码之前,我们要先对 Kafka 日志有一个大体的认识

这也是阅读源码的关键,一步一步来

前面我们聊到了 Kafka 的生产端的整体架构

可以看到,我们每一个 Topic 都可以分为多个 Partition ,而每一个 Partition 对应着一个 Log

但这里会存在两个问题,如果我们的数据过大

  • 一个 Log 能装下吗?
  • 就算能装下,插入/查询速度怎么保证?

所以,Kafka 在这里引入了日志分段(LogSegment )的概念,将一个 Log 切割成多个 LogSegment 进行存储

实际上,这里的 LogLogSegment 并不是纯粹的物理意义上的概念

  • Log 对应的文件夹
  • LogSegment 对应磁盘上的一个日志文件和两个索引文件 日志文件:以 .log 为文件后缀两个索引文件: 偏移量索引文件(以 .index为文件后缀)时间戳索引文件(以.timeindex为文件后缀)

这里有个重点要记一下:每个 LogSegment 都有一个基准偏移量 baseOffset,用来表示当前 LogSegment 第一条消息的 offset

日志和索引文件命名都是按照基准偏移量进行命名,所以日志整体架构如下:

这里我们简单介绍下这个日志是怎么搜索的,后面会深入源码细聊

二、日志源码

我们回顾一下上篇文章的整体流程图:

我们可以看到,消息的处理是通过 KafkaApis 来进行的,日志持久化通过 case ApiKeys.PRODUCE => handleProduceRequest(request)

本篇我们也围绕这个方法展开

1、授权校验

def handleProduceRequest(request: RequestChannel.Request) {

  // authorizedRequestInfo:存储通过授权验证的主题分区和对应的内存记录。
  val authorizedRequestInfo = mutable.Map[TopicPartition, MemoryRecords]()
  for ((topicPartition, memoryRecords) <- produceRequest.partitionRecordsOrFail.asScala) {
      if (!authorize(request.session, Write, Resource(Topic, topicPartition.topic, LITERAL)))
    		// 未授权的
        unauthorizedTopicResponses += topicPartition -> new PartitionResponse(Errors.TOPIC_AUTHORIZATION_FAILED)
      else if (!metadataCache.contains(topicPartition))
        nonExistingTopicResponses += topicPartition -> new PartitionResponse(Errors.UNKNOWN_TOPIC_OR_PARTITION)
      else
        try {
          // 授权的
          ProduceRequest.validateRecords(request.header.apiVersion(), memoryRecords)
          authorizedRequestInfo += (topicPartition -> memoryRecords)
        } catch {
          case e: ApiException =>
            invalidRequestResponses += topicPartition -> new PartitionResponse(Errors.forException(e))
        }
    }
}

2、消息添加

  • 【重点】timeout:超时时间
  • 【重点】requiredAcks:指定了在记录追加到副本后需要多少个副本进行确认,才认为写操作成功 0: 不需要任何副本的确认1: 只需要主副本确认-1 或 all: 需要所有副本的确认
  • internalTopicsAllowed:是否允许将记录追加到内部主题
  • isFromClient:请求是否来自客户端
  • 【重点】entriesPerPartition:包含了通过授权验证的主题分区和对应的内存记录
  • responseCallback:回调函数,在记录追加完成后,会调用该回调函数发送响应给客户端。
  • recordConversionStatsCallback:处理记录转换统计信息的逻辑
replicaManager.appendRecords(
        timeout = produceRequest.timeout.toLong,
        requiredAcks = produceRequest.acks,
        internalTopicsAllowed = internalTopicsAllowed,
        isFromClient = true,
        entriesPerPartition = authorizedRequestInfo,
        responseCallback = sendResponseCallback,
        recordConversionStatsCallback = processingStatsCallback)

我们主要关心这三个参数即可:timeoutrequiredAcksentriesPerPartition,其余的目前不太重要

def appendRecords(timeout: Long,
                  requiredAcks: Short,
                  internalTopicsAllowed: Boolean,
                  isFromClient: Boolean,
                  entriesPerPartition: Map[TopicPartition, MemoryRecords],
                  responseCallback: Map[TopicPartition, PartitionResponse] => Unit,
                  delayedProduceLock: Option[Lock] = None,
                  recordConversionStatsCallback: Map[TopicPartition, RecordConversionStats] => Unit = _ => ()) {
   // 校验当前的ACK
   if (isValidRequiredAcks(requiredAcks)) {
      // 记录起始时间
      val sTime = time.milliseconds
      // 追加本地日志
      val localProduceResults = appendToLocalLog(internalTopicsAllowed = internalTopicsAllowed,
        isFromClient = isFromClient, entriesPerPartition, requiredAcks)
   }
}

// 允许当前的ACK为1、0、-1
private def isValidRequiredAcks(requiredAcks: Short): Boolean = {
  requiredAcks == -1 || requiredAcks == 1 || requiredAcks == 0
}

这里的追加本地日志就是我们本篇的重点

2.1 获取 Partition

private def appendToLocalLog(internalTopicsAllowed: Boolean,
                             isFromClient: Boolean,
                             entriesPerPartition: Map[TopicPartition, MemoryRecords],
                             requiredAcks: Short): Map[TopicPartition, LogAppendResult] = {
  val partition = getPartitionOrException(topicPartition, expectLeader = true)
}

// 根据给定的主题分区获取对应的分区对象
def getPartitionOrException(topicPartition: TopicPartition, expectLeader: Boolean): Partition = {
   	// 获取Partition并匹配
    getPartition(topicPartition) match {
      case Some(partition) =>
        if (partition eq ReplicaManager.OfflinePartition)
          throw new KafkaStorageException()
        else
          partition
      case None if metadataCache.contains(topicPartition) =>
        if (expectLeader) {
          throw new NotLeaderForPartitionException()
        } else {
          throw new ReplicaNotAvailableException()
        }
    }
  }

2.2 向 Leader 追加日志

val info = partition.appendRecordsToLeader(records, isFromClient, requiredAcks);

def appendRecordsToLeader(records: MemoryRecords, isFromClient: Boolean, requiredAcks: Int = 0): LogAppendInfo = {
     val info = log.appendAsLeader(records, leaderEpoch = this.leaderEpoch, isFromClient,
            interBrokerProtocolVersion)
}

def appendAsLeader(records: MemoryRecords, leaderEpoch: Int, isFromClient: Boolean = true,
                     interBrokerProtocolVersion: ApiVersion = ApiVersion.latestVersion): LogAppendInfo = {
    append(records, isFromClient, interBrokerProtocolVersion, assignOffsets = true, leaderEpoch)
  }

2.2.1 是否创建 segment

这里就到了我们一开始图中的 LogSegment

 val segment = maybeRoll(validRecords.sizeInBytes, appendInfo);

 private def maybeRoll(messagesSize: Int, appendInfo: LogAppendInfo): LogSegment = {
   	// 如果应该滚动,创建一个新的segment
    // 反之,则返回当前的segment
    if (segment.shouldRoll(RollParams(config, appendInfo, messagesSize, now))) {
      appendInfo.firstOffset match {
        case Some(firstOffset) => roll(Some(firstOffset))
        case None => roll(Some(maxOffsetInMessages - Integer.MAX_VALUE))
      }
    } else {
      segment
    }
 }

一共有六个条件,触发这六个条件,就会重新创建一个 segment

  • timeWaitedForRoll(rollParams.now, rollParams.maxTimestampInMessages) > rollParams.maxSegmentMs - rollJitterMs :判断时间等待是不是超时
  • size > rollParams.maxSegmentBytes - rollParams.messagesSize:当前 segment 是否有充足的空间存储当前信息
  • size > 0 && reachedRollMs :当前日志段的大小大于0,并且达到了进行日志分段的时间条件reachedRollMs
  • offsetIndex.isFull :偏移索引满了
  • timeIndex.isFull:时间戳索引满了
  • !canConvertToRelativeOffset(rollParams.maxOffsetInMessages):无法进行相对偏移的转换操作
class LogSegment private[log] (val log: FileRecords,
                               val offsetIndex: OffsetIndex,
                               val timeIndex: TimeIndex,
                               val txnIndex: TransactionIndex,
                               val baseOffset: Long,
                               val indexIntervalBytes: Int,
                               val rollJitterMs: Long,
                               val time: Time) extends Logging {

  def shouldRoll(rollParams: RollParams): Boolean = {
    val reachedRollMs = 
    timeWaitedForRoll(rollParams.now, rollParams.maxTimestampInMessages) >    rollParams.maxSegmentMs - rollJitterMs
    size > rollParams.maxSegmentBytes - rollParams.messagesSize ||
      (size > 0 && reachedRollMs) ||
      offsetIndex.isFull || timeIndex.isFull || !canConvertToRelativeOffset(rollParams.maxOffsetInMessages)
}

整体来看,六个条件也比较简单,我们继续往后看

2.2.2 创建 segment

appendInfo.firstOffset match {
  // 存在第一个偏移量
  case Some(firstOffset) => roll(Some(firstOffset))
  // 不存在第一个偏移量
  case None => roll(Some(maxOffsetInMessages - Integer.MAX_VALUE))
}

2.2.2.1 文件路径校验
def roll(expectedNextOffset: Option[Long] = None): LogSegment = {
  
  // 获取最新的offset
  val newOffset = math.max(expectedNextOffset.getOrElse(0L), logEndOffset)
  // 获取日志文件路径
  val logFile = Log.logFile(dir, newOffset)
  // 获取偏移量索引文件路径
  val offsetIdxFile = offsetIndexFile(dir, newOffset)
  // 获取时间戳索引文件路径
  val timeIdxFile = timeIndexFile(dir, newOffset)
  // 获取事务索引文件路径
  val txnIdxFile = transactionIndexFile(dir, newOffset)
  
  // 对路径列表进行遍历,如果文件存在,则将其删除。
  for (file <- List(logFile, offsetIdxFile, timeIdxFile, txnIdxFile) if file.exists) {
    Files.delete(file.toPath)
  }
}

2.2.2.2 segment 参数
  • dir:日志段所在的目录
  • baseOffset:日志段的基准偏移量
  • config:日志的配置信息
  • time:时间对象,用于处理时间相关的操作。
  • fileAlreadyExists:指示日志文件是否已经存在
  • initFileSize:初始文件大小
  • preallocate:是否预分配文件空间
  • fileSuffix:文件后缀
val segment = LogSegment.open(dir,
  baseOffset = newOffset,
  config,
  time = time,
  fileAlreadyExists = false,
  initFileSize = initFileSize,
  preallocate = config.preallocate)

2.2.2.3 生成 segment
new LogSegment(
  // 生成日志文件
  FileRecords.open(Log.logFile(dir, baseOffset, fileSuffix), fileAlreadyExists, initFileSize, preallocate),
  // 生成偏移量索引
  new OffsetIndex(Log.offsetIndexFile(dir, baseOffset, fileSuffix), baseOffset = baseOffset, maxIndexSize = maxIndexSize),
  // 生成时间戳索引
  new TimeIndex(Log.timeIndexFile(dir, baseOffset, fileSuffix), baseOffset = baseOffset, maxIndexSize = maxIndexSize),
  // 生成事务索引
  new TransactionIndex(baseOffset, Log.transactionIndexFile(dir, baseOffset, fileSuffix)),
  // 基准偏移量
  baseOffset,
  indexIntervalBytes = config.indexInterval,
  rollJitterMs = config.randomSegmentJitter,
  time)

这里有一个重点需要关注一下,那就是 mmap 的零拷贝

OffsetIndexTimeIndex 他们继承 AbstractIndex ,而 AbstractIndex 中使用 mmp 作为 buffer

class OffsetIndex(_file: File, baseOffset: Long, maxIndexSize: Int = -1, writable: Boolean = true) extends AbstractIndex[Long, Int](_file, baseOffset, maxIndexSize, writable) 
    

abstract class AbstractIndex{
   protected var mmap: MappedByteBuffer = {};
}

另外,这里先提一个知识点,后面会专门写一篇文章来分析一下

我们索引在查询的时候,采用的是二分查找的方式,这会导致 缺页中断,于是 kafka 将二分查找进行改进,将索引区分为 冷区 和 热区,分别搜索,尽可能保证热区的页在 Page Cache 里面,从而避免缺页中断。

当我们的 segment 生成完之后,就返回了

2.2.3 向 segment 添加日志

segment.append(largestOffset = appendInfo.lastOffset,
          largestTimestamp = appendInfo.maxTimestamp,
          shallowOffsetOfMaxTimestamp = appendInfo.offsetOfMaxTimestamp,
          records = validRecords)

def append(largestOffset: Long,
             largestTimestamp: Long,
             shallowOffsetOfMaxTimestamp: Long,
             records: MemoryRecords): Unit = {
  if (records.sizeInBytes > 0) {
    	// 添加日志
      val appendedBytes = log.append(records)
  }
  // 当累加超过多少时,才会进行索引的写入
  // indexIntervalBytes 默认 1048576 字节(1MB)
  if (bytesSinceLastIndexEntry > indexIntervalBytes) {
    // 添加偏移量索引
    offsetIndex.append(largestOffset, physicalPosition)
    // 添加时间戳索引
    timeIndex.maybeAppend(maxTimestampSoFar, offsetOfMaxTimestamp)
    // 归0
    bytesSinceLastIndexEntry = 0
  }
  // 累加
  bytesSinceLastIndexEntry += records.sizeInBytes
}

// lastOffset + 1
updateLogEndOffset(appendInfo.lastOffset + 1)

2.2.3.1 稀疏索引

kafka 中的偏移量索引和时间戳索引都属于稀疏索引

何为稀疏索引?

正常来说,我们会为每一个日志都创建一个索引,比如:

日志  索引
1     1
2     2
3     3
4     4

但这种方式比较浪费,于是采用稀疏索引,如下:

日志  索引
1     1
2			
3			
4			
5     2
6
7
8

当我们根据偏移量索引查询 1 时,可以查询到日志为 1 的,然后往下遍历搜索想要的即可。

2.2.3.2 偏移量索引
offsetIndex.append(largestOffset, physicalPosition)
 
def append(offset: Long, position: Int) {
  inLock(lock) {
    // 索引位置
    mmap.putInt(relativeOffset(offset))
    // 日志位置
    mmap.putInt(position)
    _entries += 1
    _lastOffset = offset
  }
}

// 用当前offset减去基准offset
def relativeOffset(offset: Long): Int = {
  val relativeOffset = offset - baseOffset
}

2.2.3.3 时间戳索引
timeIndex.maybeAppend(maxTimestampSoFar, offsetOfMaxTimestamp)

def maybeAppend(timestamp: Long, offset: Long, skipFullCheck: Boolean = false) {
    inLock(lock) {
      if (timestamp > lastEntry.timestamp) {
        // 添加时间戳
        mmap.putLong(timestamp)
        // 添加相对位移(偏移量索引)
        mmap.putInt(relativeOffset(offset))
        _entries += 1
        _lastEntry = TimestampOffset(timestamp, offset)
      }
    }
}

2.2.3.4 索引总结

我们的偏移量索引如图下所示:

  • 当我们查询一个消息时,比如消息位移为 23 的 根据二分查找找到偏移量索引下标 22利用上述我们偏移量 Map 的存储,得到其日志位置 RecordBatch:firstOffset=23 position=762再根据日志位置,找到真正存储日志的地方

我们的时间戳索引如图下所示:

  • 基本和我们的偏移量索引类似,只是增加了一层二分查找

2.2.4 flush刷新

在我们前面添加完之后,我们的数据仅仅是写到 PageCache 里面,需要进行 flush 将其刷新到磁盘中

// 未刷新消息数(unflushedMessages)超过配置的刷新间隔(flushInterval)
if (unflushedMessages >= config.flushInterval){
  flush()
}

def flush() {
    LogFlushStats.logFlushTimer.time {
      // 日志刷新
      log.flush()
      // 偏移量索引刷新
      offsetIndex.flush()
      // 时间戳索引刷新
      timeIndex.flush()
      // 事务索引刷新
      txnIndex.flush()
    }
  }

2.3 Follow 获取日志

同样,我们的 Follow 在获取日志时,和我们 Leader 添加日志时一样的方法

三、流程图

四、总结

这一篇我们介绍了 Kafka 中日志时如何持久化的以及 Kafka 日志中包括什么数据

鲁迅先生曾说:独行难,众行易,和志同道合的人一起进步。彼此毫无保留的分享经验,才是对抗互联网寒冬的最佳选择。

其实很多时候,并不是我们不够努力,很可能就是自己努力的方向不对,如果有一个人能稍微指点你一下,你真的可能会少走几年弯路。

我是爱敲代码的小黄,阿里巴巴淘天集团Java开发工程师,双非二本,培训班出身

通过两年努力,成功拿下阿里、百度、美团、滴滴等大厂,想通过自己的事迹告诉大家,努力是会有收获的!

双非本两年经验,我是如何拿下阿里、百度、美团、滴滴、快手、拼多多等大厂offer的?

我们下期再见。

从清晨走过,也拥抱夜晚的星辰,人生没有捷径,你我皆平凡,你好,陌生人,一起共勉。

#面试##Java##校招##社招##kafka#
全部评论

相关推荐

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;今年5月至9月,我有幸进到字节抖音进行为期4个月的暑期实习,成为一名前端开发实习生。我相信有很多牛u跟我一样,刚进去的时候不知道到底该做些什么,该怎么上手,怎么总结。做的活大部分是dirty&nbsp;work,或者是业务需求,如何沉淀技术亮点?下面我从我自己个人的一些经验出发,总结几点我认为比较重要的,可能对大家有所帮助。1.在landing阶段明确自己实习的目标&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;如果你是一段日常实习或者是非秋招阶段前的暑期实习,那么大概率是为了一段漂亮的履历丰富自己的简历,那么你实习的目标就是在做需求并精进自己业务能力的同时深入挖掘每一个大需求中的亮点,并做好总结。&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;如果你是秋招前的暑期实习,那么首先需要明确自己的目标是什么:是为了一段履历,为后续找工作铺路?还是为了争取转正名额,最后留在这里?这些需要你在进入公司后的landing期内做出最初的决定。&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;一般所有公司对于实习生都会有一段融入时间。以字节为例,我们部门的landing期为两周,第一周会看各种文档,比如团队技术栈、参与产品的业务介绍、产品功能介绍以及各种培训考试;第二周会上手写一个小demo,熟悉组件库、react和基本的技术框架;之后就会慢慢上手接触需求。&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;业务上:在这两周,要多跟mt或者ld了解团队业务背景,比如大团队细分为哪些方向,不同的方向在做什么业务和产品,在整体链路中属于哪一环,自己参与到的产品当前的进展和本Q目标等等,业务内容是否符合你的期望,产品目标是否体现出了这个产品或者部门的发展前景,这些跟自己未来的发展也有强关联(比如当前已经过于成熟,没什么可做的了,那也许会有裁员风险;比如当前业务处于初期,那么后面入职后可能很累,或者压力很大,不确定性比较强)。&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;技术上:在这里工作是否有很多技术项可以接触?如果要作为第一段正式工作,我认为一定是要能带给你技术成长的才更适合,如果你看完了他们的文档,通过交流也没有发现有什么技术沉淀,那么建议以此作为跳板去找更优秀的工作。&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;团队氛围上:团队氛围很大程度上决定了你未来工作的体验。团队成员工作状态怎么样(是否比较有活力,上下班时间),团队工作外的氛围如何(成员间关系、周末活动、团建),团队技术氛围怎么样(平时会不会注重体验做专项优化、会不会有自己的想法推动落地),这些在landing期内通过观察和与同事沟通就能知道的差不多。我在实习期间,深刻的感受到了团队的技术氛围很浓厚,并且大家周末会约着打羽毛球、健身、特种兵徒步,也参与到了团建中,这种团队氛围让我产生了很强的留在这里的欲望。并且我认为周围的人都是很有思想的,他们会真正产生想法并推动落地,让我觉得有很多可以学习的点。&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;在这段时间明确自己的目标,后续的实习才好安排主要内容。2.在不同的阶段明确主体目标&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;如果你的目标是转正,那么要跟ld确定好是否有转正hc,是等额还是差额。如果没hc那就把重点放在秋招把。&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;如果目标是转正,我的经验是前期(8月前)认真实习,做好需求,并保证质量,这个阶段的努力是给ld和mt留下好的印象分。后期(8月后到答辩)匀一部分时间出来给笔试面试和复习,这段时间的目标是为了并行秋招和转正,所有两边都要兼顾好,前期打下了基础并做好了总结,后面写文档、复盘、整理面经都会顺手很多。3.主动跟ld或mt要需求,主动约一对一总结会&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;我认为避免dirty&nbsp;work的方法之一就是主动要某一块的需求。当你做完第一个小需求的时候,就可以主动跟mt去要大型需求或者其他活了,比如你看中了那一块,希望参与,或者觉得有哪些可以优化的点,都可以跟他们去提出想法。一来是凸显出你的主动性,二来是把选择工作内容的权力尽可能的掌握在自己手里(也许有用)。我是经常跟带我的同学要需求,实习期间一共三个需求,全是大需求,所以后面就有的沉淀。&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;主动跟老板约一对一总结会。这是你在老板面前表现的机会,老板不一定会知道你最近在做什么的,所以你的情况要主动去跟他同步。大概两周一次或者一个月一次,这样的频率就是每次会议都有内容可以说。&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;这里有我自己的一些经验(也来自于我老板给我的建议):&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;①提前书写总结会的文档。可以从近期工作内容和产出、收获、自己本阶段发现的问题和不足、下一阶段的规划展开。&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;②总结文档🈲空话。最好是你列举的每一点都能写上一到两个具体case,如git使用不熟练不规范,出现了xxxx问题,原因是xxxx。有case非常重要!!!&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;③文档一定要体现自己的思考。ld可能不了解你做的事情,但从你的总结中看到自己的思考,这点是与你个人的发展潜力挂钩的,综合评判的时候一定有帮助。4.及时总结&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;我在实习期间,写了四个需求总结,写了两个技术方案,两个团队分享。个人的习惯是在开发完一个需求后,重新复盘自己的开发思路、遇到的问题、新学习到的技术框架或技术栈,还能优化的点之类的。这些总结文档老板是会看的,所以当他看到你平时的总结习惯,特别是里面自己的一些思考的时候,一定会给你加分的。&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;写总结还有一个好处,你所有的实现细节、思路都在里面,这就是你后面转正文档的主要内容,前期写好,后期写转正答辩文档就轻而易举了。5.需求全是业务需求,如何沉淀技术亮点写上简历?&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;我想这个问题应该是很多uu遇到的。我自己有几个小建议:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;①多看一下其他同学的内容,把技术难点偷一部分。一个大型需求有几个同学同时开发,那么其他同学那边可能会有能偷的点,比如服务端渲染之类的技术点,抽空去看这部分的代码,去问他实现思路和原理,自己再去拓展了解和延申,这就是简历的一个亮点了。&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;②多去思考自己的业务逻辑或者代码有什么可以优化的点,比如复杂的计算能不能开启web&nbsp;worker优化?真正开发中没用上可以自己私下尝试一下,自己没有尝试过可以多去了解一下大致的实现细节,只要能讲明白,那不也可以作为简历的一个亮点吗?毕竟面试官可不知道你做了没有&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;作为实习生,老板或者mt不会一上来就给你很难的需求的,也不会接触到太多的技术,想要有亮点,一定要多去自己发现和思考。平时做好总结,多多沉淀,不管什么活都认真干好,一定会在老板们心中留下好印象。对你后续的简历书写、秋招都有好处。&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;最后祝大家实习顺利、秋招顺利。四面八方offer来!!!&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;欢迎大家点赞&nbsp;&nbsp;收藏&nbsp;&nbsp;评论&nbsp;&nbsp;留言,方便的话送朵花花,哥们想要个领航员挂饰。欢迎评论区交流。以上是自己的个人经验,希望帮到大家#我的求职思考##不给转正的实习,你还去吗##实习##前端##想实习转正,又想准备秋招,我该怎么办#
平安海拉尔:要是能在实习前看到这篇帖子就好了
投递字节跳动等公司10个岗位 我的求职思考 不给转正的实习,你还去吗
点赞 评论 收藏
分享
6 22 评论
分享
牛客网
牛客企业服务