注: 此系列内容来自网络,未能查到原作者。感觉不错,在此分享。
pprof应用流程:https://time.geekbang.org/column/article/410205 https://time.geekbang.org/column/article/408529
pprof(堆、哪个办法占用cpu工夫多,耗时、协程数、零碎线程数、阻塞耗时block、互斥锁耗时mutex) 。go tool pprof 连贯进行pprof采集,默认采集30秒。pprof可剖析go程序cpu占用过高问题或零碎线程数过多cpu负载高。采样图剖析:采样图中,矩形框面积越大,阐明这个函数的累积采样工夫越大。如果一个函数剖析采样图中的矩形框面积很大,这时候咱们就要认真剖析了,因为很可能这个函数就有须要优化性能的中央。火焰图剖析:每一列代表一个调用栈,每一个格子代表一个函数。纵轴展现了栈的深度,依照调用关系从上到下排列。最上面的格子代表采样时,正在占用 CPU 的函数。调用栈在横向会依照字母排序,并且同样的调用栈会做合并,所以一个格子的宽度越大,阐明这个函数越可能是瓶颈。耗时剖析(终端交互):top命令默认是按flat%排序的。做性能剖析时,先依照cum来排序,top -cum,能够直观地看到哪个函数总耗时最多,而后参考该函数的本地采样工夫和调用关系。判断是该函数性能耗时多,还是它调用的函数耗时多。本地采样工夫占比很小,然而累积采样工夫占比很高,这个函数耗时多是因为调用了其余函数,它们本身简直没有耗时。而后用`list 办法名`命令查看具体的外部耗时状况。内存剖析:内存剖析同上,本地采样内存占比很小,然而累积采样内存占比很高,这个函数多是因为调用的其余函数费内存,它们本身简直没有内存耗费。go trace :能够剖析GC问题,依据GC反查到哪个办法在大量生成内存触发GC。 但如果您想跟踪运行迟缓的函数,或找到CPU工夫破费在哪,这个工具就不适合了。您应该应用go tool pprof,它能显示在每个函数中破费的CPU工夫的百分比。 go tool trace更适宜找出程序在一段时间内正在做什么,而不是总体上的开销。
go程序启动流程:
初始化并绑定m0与g0,m0是主线程,程序启动必然会有个主线程m0,g0负责调度:调用schedule()函数;创立P,绑定m0和p0,先创立GOMAXPROCS个P,存在sched闲暇链表(Pidle);m0的g0会创立一个指向runtime.main()的g,并放到p0的本地队列;runtime.main():启动sysmon线程、启动GC协程、执行init(各种init函数)、执行 main.main 函数;新G将唤醒Pidle里的P,以更好地散发工作。P会找一个自旋状态的M/唤醒睡眠的M/创立M与之关联,而后切换g0进行schedule调度。
golang调度:runnext > 本地队列 > 全局队列 > 网络 > (从其余P的本地队列中)偷取
1、从本地队列拿,2、没有从全局队列里拿一半回本地队列,3、从网络轮询器里拿并Inject到全局队列,4、从别的P偷一半放到本地,注:每个P每61次都会从全局队列里拿一个,防全局队列里的G饿死。newm:创立一个工作线程,调用mstart,再调用minit和mstartm0做信号初始化工作(注册信号抢占函数),最初调用schedule()。每个M都有一个schedule()做死循环的用g0进行G的调度。当G运行完结时,会先放到p.gFree,最多64个,满了放一半到sche.gFree闲暇链表里,当再go fun时会先从gFree里拿旧的G,没有再创立新的G对象。schedule()函数:一开始获取的_g_ 是g0来的,从g0上获取对应的m到p拿到G后,调用gogo 从g0栈切换到G栈后执行工作,执行完后调用goexit到mcall 把G栈切换回g0栈,最初调用goexit0把G放回gFree。而后从头再来,获取_g_拿到g0循环这样解决。g0的作用是帮助调度的协程。g->g0 10us、findG 20-100us、 g0->next G 10us
gopark的作用:https://blog.csdn.net/u010853261/article/details/85887948
解除以后G和M的绑定关系,将以后G状态机切换为_Gwaiting;调用一次schedule(),调度器P发动一轮新的调度;
mcall()的作用:保留以后协程信息(PC/SP存到g->sched),调用goready时能复原现场; 以后线程从g堆栈切换到g0堆栈;在g0的堆栈上执行函数fn(g);gopark里的park_m是在切换到g0堆栈上后调度schedule(),schdeule()能够从g0开始进行G的调度;
goready的作用:将G的状态机切换到_Grunnable,而后退出到P的调度器的本地队列,期待P进行调度。
对于mainG,执行完用户定义的main函数的所有代码后,间接调用exit(0)退出整个过程,很王道,不论其余G是否解决实现。对于一般G在执行完代码,先是跳转到提前设置好的 goexit 函数的第二条指令,而后调用 runtime.goexit1,接着调用 mcall(goexit0),会切换到 g0 栈,运行 goexit0 函数,清理 G 的一些字段,并将其增加到 G 缓存池(gFree)里,而后进入 schedule 调度循环。到这里,一般 goroutine 才算完成使命。
Sysmon监控线程:
处于 _Prunning 或者 _Psyscall 状态的P,如果上一次触发调度的工夫曾经过来了 10ms,咱们会通过 runtime.preemptone 抢占以后处理器。处于_Psyscall 状态的P,当零碎调用超过10ms,且队列有其余G要解决或不存在闲暇处理器,Sysmon会调用runtime.handoffp让出P的使用权,把M和G独立进来运行。留神:抢占P的使用权和让出P的使用权是不一样的。留神零碎调用时线程池耗尽问题,Go里M最多1w个。 1.13依赖栈增长检测代码形式:编译器在有显著栈耗费的函数头部插入一些检测代码,用g.stackguard0判断是否须要进行栈增长,如果g.stackguard0被设置为非凡标识runtime.stackPreempt便不会执行栈增长而去执行一次schedule()。空的for循环并没有调用函数,就没机会执行栈增长检测代码,他并不知道GC在期待他让出。 1.14基于信号的抢占调度:在创立一个工作线程时,会调用mstart,调用minit做信号初始化(注册信号抢占函数),产生抢占时会调用preemptone做信号异步抢占,preemptone->preemptM->signalM(向指定M发送sigPreemt信号)->SYS_tgkill。收到信号后会调用asyncPreempt2调mcall和gopreempt_m2分钟触发次GC。若刚开始生成大量堆内存,GC后的残余内存变多 GOGC为100,则1G变2G才触发,2G变4G才触发。所以要定期2分钟清理。周期性扫描网络轮询器,把监听到数据的goroutine注入到全局队列
golang Mutex同步原语: https://my.oschina.net/u/142293/blog/5012739
互斥锁在失常状况下是把G切换至_Gwaiting,期待锁的持有者唤醒协程,把G gopark放到semaroot的sudog链表里挂着,解锁时会调用goready期待P调度,协程锁是基于线程锁的,可能会陷入内核挂起。读写锁是写优先于读的,防读锁过多导致写始终阻塞,读多写少RWMutex。写多读少mutex就行。semaRoot(semtable会有251个semaRoot均衡树,每一个sync.Mutex的sema地址对应一个semaRoot,雷同的锁的sudog由链表串起来) semaRoot则是队列构造体,外部是堆树,把和以后G关联的sudog放到semaRoot里,而后把G的状态改为_Gwaiting,调用调度器执行别的G,此时以后G就进行执行了。始终到被调度器从新调度执行,会首先开释sudog而后再去执行别的代码逻辑,semaRoot里有runtime.mutex线程锁的,要保障在面临多线程并发时,数据同步的问题。sync.Mutex与runtime.mutex区别,Mutex挂起协程,不肯定阻塞线程。mutex是挂起线程,调度下一个线程执行。sync.Mutex=>gopark(),runtime.mutex=>futex()。sync.Mutex是设计给goroutine的,但goroutine是在用户空间实现的,但内核只能看见一组线程。协程同步的根底是线程同步,只是可能自旋不必挂起线程了,临界区内实现期待队列这类敏感数据的操作就行了。就是说runtime.mutex是线程锁,其底层是futex零碎调用,但加锁工夫往往很短。留神goparkUnlock是在这里解锁线程锁的。
golang defer原理:https://zhuanlan.zhihu.com/p/261840318
defer语句在以后G构造体的g._defer链表把defer构造体串起来,新defer是推入链表头部的。在有defer的函数开端会插入runtime.deferreturn函数来调用defer链;函数return执行流程:1.先给返回值赋值(匿名和有名返回值),2.以后G的g._defer链表的头开始执行defer构造里指向的函数,3.包裹函数return返回。defer在1.14版本的优化:编译阶段把defer代码块插入尾部(openCodedDefer 相似内联),但不适宜循环中defer,循环中defer还是会插入g._defer链表
golang panic/recover原理:g._defer链表、g._panic链表(https://draveness.me/golang/docs/part2-foundation/ch05-keywor...)
编译器会做转换关键字的工作,panic和recover转换成runtime.gopanic和runtime.gorecover;defer转换成runtime.deferproc函数;panic时,执行gopanic函数,创立新runtime._panic增加到所在G的g._panic链表头部,而后遍历g._defer链调用提早函数,最初调用 fatalpanic 执行g._panic停止整个程序;defer与panic:先解决完g._defer链上的工作,结束后再执行g._panic再退出整个程序,停掉执行中的程序。panic只会触发以后G的defer,recover只有g._panic链有值时执行才会失效,即recover写在最开始的defer语句中;panic容许在defer中嵌套屡次调用;panic与recover:panic时调用g._defer链,若遇到了gorecover会将_panic.recovered标记成true并返回panic的参数,会跳到deferreturn函数持续遍历g._defer链,不再执行fatalpanic停止程序,就从panic中复原并执行失常的逻辑;若没遇到gorecover会遍历g._defer链,最初调fatalpanic停止程序。
golang Channel构造:
lock(runtime.mutex 线程锁)、buff、qcount(缓冲区现有多少元素)、buffSize(缓冲区总大小)、closed(敞开状态)、sendx/recvx(队列下标用于数组循环队列)、sendq(期待写音讯队列)、recvq(期待读音讯队列)、elemtype(元素的类型)
写:如果缓冲区中没有空余地位,将待发送数据写入G,将以后G退出sendq,进入睡眠,期待被读goroutine唤醒;
读:如果缓冲区中没有数据,将以后goroutine退出recvq,进入睡眠,期待被写goroutine唤醒;
Channel里是有runtime.mutex线程锁的,读写时会lock一下,因为读写存在缓冲区的设计,并没有间接应用sync.Mutex,而是在mutex的根底上,减少waitq、writeq本人实现了一套排队逻辑,保障多线程并发。
golang Map构造:hashmap,是应用数组+bmap链表实现的,用拉链法打消hash抵触
key通过hash后失去一个数,其低8位用于抉择bucket桶,高8位用于放在bmap的[8]uint8数组中,用于疾速碰撞试错。而后一个bmap指向下一个bmap即溢出桶(拉链)。
map并发问题:入源代码写抵触 h.flags ^= hashWriting。hash抵触后元素是优先放头部,新写数据被马上拜访的概率越大
扩容:扩容桶会翻倍(B+1),渐进式扩容,当拜访到具体的某个bucket的时候,会把旧bucket中的数据转移到新的bucket中。一个bucket一个bucket的进行迁徙,能够避免被锁住长时间不可拜访(性能抖动)。扩容因子: hmap.count>桶数量 (2^B)*6.5(不包含溢出桶),无奈缩容。等量扩容(伪缩容):为了缩小溢出桶应用(删数据),排列的更加紧凑。非溢出桶无奈主动缩容,需手动解决缩容:创立一个新的 map 并从旧的 map 中复制元素。
Sync.map构造:外部有两个map,一个read、一个dirty,读时先无锁读read,当read里没有时上锁读dirty
Sync.Pool原理:每个P创立一个本地对象池poolLocal,尽量减少并发抵触;每个poolLocal都有一个private对象,优先存取private对象,防止简单逻辑;在Get和Put期间,利用pin锁定以后P,避免G被抢占,造成程序凌乱。获取对象期间,利用对象窃取机制,从其余P的本地对象池以及victim中获取对象。充分利用CPU Cache个性,晋升程序性能。
golang Select原理:https://www.codenong.com/cs106626574/
在一个select中,所有case语句会形成一个scase构造体的数组,Select和rselect都是做了简略的初始化参数 selectgo才是外围。selectgo的做的事件是:
1、打乱传入的case构造体数组程序(select的随机读写channel的实现形式),而后锁住其中的所有的channel。
2、遍历所有的channel,查看其是否可读或者可写。
3、其中的channel可读或者可写,则解锁所有channel,返回对应的channel数据,都没读写但有default语句,返回default语句对应的scase并解锁所有的channel。
4、没有channel可读写,没有default语句,将以后运行的groutine阻塞,退出到以后所有channel的期待队列中去,而后解锁所有channel,期待被唤醒。
5、有阻塞的channel读写ready了则唤醒,再次加锁所有channel,遍历所有channel找到对应的channel和G,唤醒G,将G从其余channel的期待队列中移除。
golang协程与epoll: https://strikefreedom.top/go-netpoll-io-multiplexing-reactor
1、当调用net.Listen时会创立listenfd并设置成非阻塞 而后调用epollCreate初始化epollfd并把 listenfd 并封装成pollDesc并调用epollCtl注册进epoll里。2、当Accept产生connfd时,办法外部会把connfd封装成pollDesc并调用epollCtl注册进epoll里。3、当Read或Write时,因为是非阻塞IO,当没有数据,没有IO事件产生时,会返回EAGAIN谬误,会调用 fd.pd.waitRead办法,把以后G进行goPark,挂在pollDesc构造体里的rg或wg字段上,期待epollWait调度。4、当scheule()或sysmon进行调度时,会调用netpoll办法进行epollWait调用,获取rdlist里有IO数据的fd所对应的pollDesc对象,把fd里的数据写入pollDesc里rg或wg挂载的G,而后返回G链表并注入进全局队列期待被P调度。
golang 类型元数据、接口与反射 :
类型元数据:有内置类型元数据和自定义类型元数据,自定义类型由根底类型元数据和uncommontype一起形容。type _type struct空接口:底层构造体为eface, 构造体内容为 type *_type和 data unsafe.Pointer非空接口:底层构造做为iface, 构造体内容为 tab *itab 和 data unsafe.Pointer 反射TypeOf与ValueOf性能差别的起因:TypeOf操作全程是在栈上进行的。ValueOf变量:会把变量显示地拷贝一份并逃逸到堆上留下拷贝变量的指针、ValueOf变量指针:会把原变量逃逸到堆上并留下原变量的指针。ValueOf减少了堆调配开销和GC开销。ValueOf里有rType指针,能够操作Type接口办法。
golang内存治理: https://www.php.cn/be/go/436885.html https://studygolang.com/articles/21025
struct{}不占用内存,编译器在内存调配时做的优化,当mallocgc发现对象size为0时,间接返回变量 zerobase 的援用,其是所有0字节的基准地址,不占据任何宽度。Golang内存治理(mcache(治理P本地的mspan数组)、mcentral(治理全局mspan)、每个arena64M(8192个page)、mheap(单位page 8KB 共512G),mheap拆分成多个arena(64M),arena又拆分成多个span,heapArena有个spans数组对应不同的mspan,每个P都有独立的mcache缩小锁开销,mspan外面依照8*2大小的object(8b,16b,32b ....)分为67种内存块,映射到对应类型的span。有多少种类型的mspan就有多少个mcentral。每个mcentral都会蕴含两个mspan的列表:没有闲暇对象或mspan曾经被mcache缓存的mspan列表(noempty mspanList),有闲暇对象的mspan列表(empty mspanList);因为mspan是全局的,会被所有的mcache拜访,所以会呈现并发性问题,因此mcentral会存在一个锁。mheap有一个mcentral数组治理136个不同类型mspan的mcentral数组。mheap治理整个512G的堆内存,heapArena治理一个64M的arena,heapArena有一个spans数组字段,用于询址,寄存span元数据,mspan治理一个span(一组pages)。bitmap区域用于示意arena区域中哪些地址保留了对象, 并且对象中哪些地址蕴含了指针。bitmap区域中一个字节对应了arena区域中的四个指针大小的内存, 即2bit对应一个指针大小的内存,标识需不需要在扫描,就是垃圾回收用的。Go没法应用P的本地缓存mcache和全局核心缓存mcentral上超过32KB的内存调配,大于32KB内存间接申请mheap,mcentral内存不够时向mheap申请若干page并把page切割成mspan单位大小。对于小内存对象,节俭内存开销,打消内存碎片。mspan:spanclass 0号标记为大于32K的内存,1-67号为8B-32KB的内存。spanclass最低位来标识是否要GC扫描:蕴含指针要GC的扫描的归为scannable这一类(0),不含指针的归为noscan这一类(1),一共分成136类。noscan是不蕴含指针的对象,没有子对象,GC实现root标记后不须要再对指针进行扫描。scan则代表含有指针,标记完root后须要遍历其指针扫描。把span分为scan和noscan的意义在于,GC扫描对象的时候对于noscan的span能够不去查看bitmap区域来标记子对象, 大幅晋升标记的效率。mspan里allocBits位图字段:用于标记mspan里的哪些内存块被调配了。gcmarkBits字段是用于GC标记阶段会对这个位图进行标记,看span里的内存块是否开释。到GC打扫阶段开释掉旧的allocBits,把标记的gcmarkBits用作allocBits ,而后给个清0的给gcmarkBits,这个操作就是内存清理。freeIndex记录着下个闲暇的内存索引块。零GC缓存原理,在GC时scan区标记完root后须要遍历其指针扫描,noscan区实现root标记后不须要再被GC扫描标记,从而进步GC扫描性能。
golang内存调配:
mallocgc流程:1、辅助GC;2、空间调配(三种内存调配策略:tiny对象(小于16B且无指针)、mcache.alloc、large span);3、位图标记:用于线性地址找到对应的span内存块;4、收尾(GC时新对象要标记,以及是否达到阀值要触发新一轮GC)
GC:主体并发增量式回收 https://golang.design/under-the-hood/zh-cn/part2runtime/ch08g...
Go语言引入并发垃圾收集器的同时应用垃圾收集调步(Pacing)算法计算触发的垃圾收集的最佳工夫,确保触发工夫不会节约计算资源,也不会超出预期的堆大小。标记打扫算法实现起来绝对简略,但容易造成碎片化,则有了mcache这种多级缓存模式。debug.SetGCPercent:用于设置一个垃圾收集比率 设置GOGC值,默认是100。新分配内存(本次堆内存的增量)和前次垃圾收集剩下的存活数据的比率达到该百分比时,就会触发垃圾收集。默认GOGC=100,增长率100%。比方1M到2M到前面500M要到1G才会触发,当存活对象越多时越难达到阈值,所以要2分钟定时GC。go的GC策略,每次回收都会造成CPU的抖动,因GC应用cpu工夫约为总工夫的25%,咱们设置GOPROC为4,则每次标记时会占用1个CPU的100%工夫,直到GC解决实现。GC时会有两种后台任务, 在GC初始化的筹备阶段会为每个P创立了markWorker, 打扫工作只有一个后盾sweeper增量进行.GCMark阶段后台任务会在须要时启动, 同时工作的markWorker数量大概是GOMAXPROCS的25%. 后盾扫描标记内存对象时CPU默认利用率是25%. 调度时启动markWorker数量为P*25%。发现内存增长特地快,标记过程太长了,对象增长太快。会限定你G的并行度,规范是25%,当初增长到了50%。这样程序会跑的慢吞吐降落了。会多了个MA(MarkAssistant)协程:标记协助者,帮助GC一块进行减速标记。若3核或者6核无奈被4整除,这时须要1个G或额定1个G的局部CPU帮助解决垃圾收集,运行时须要占用某个CPU的局部工夫,每实现一定量工作时会计算是否达到fractionalUtilizationGoal,如果达到了FractionalWoker就能够让出cpu给其余协程用。应用Fractional模式的协程帮助保障CPU利用率达到25%。GC触发机会:手动调用,runtime.GC 、sysmon,监控线程2分钟调用、runtime.mallocgc(),申请内存时判断堆大小是否达到阈值(新分配内存比上次GC存活数据达到GOGC比率)优化GC频率:优化内存申请速度,尽可能少申请内存,复用已申请的内存。管制(协程的数量)、缩小(缩小内存申请次数,缩小slice map 动静扩容)、复用(sync.Pool缓存复用长期对象)、内存对齐(按占字节最大的类型来做对齐边界,压缩内存占用空间),尽可能地管制堆内存增长率从而缩小GC启动。 或调整GOGC参数的值,增大触发阈值。
GC流程:写屏障只有在标记阶段启用 https://golang.design/under-the-hood/zh-cn/part2runtime/ch08gc/
GC标记时会stopTheWorld启动混合写屏障(栈空间不启动,堆空间启动),而后startTheWord。再而后`标记工作`要从数据段和协程栈的root节点开始退出队列而后追踪到堆上的节点.使其变黑并始终放弃,过程不须要STW. 实现标记后会stop the world敞开混合写屏障。GC标记阶段堆栈上新创建的对象都要标记成彩色。强三色不变性:彩色对象援用的必须是灰色,弱三色不变性:所有被彩色对象援用的红色对象都有被灰色援用或间接援用.
GCMark标记筹备阶段,为并发标记做筹备工作,启动混合写屏障 赋值器处于 STW状态
GCMarK扫描标记阶段,与赋值器并发执行,赋值器处于 并发状态 占用GOMAXPROCS的25%的gcBgMarkWorker
GCMarkTermination标记终止阶段,保障一个周期内标记工作实现,敞开混合写屏障 赋值器这时处于 STW状态
GCoff内存打扫阶段,将须要回收的内存偿还到堆中,写屏障敞开 赋值器处于并发状态,GC没有运行sweep在后盾运行,写屏障没有启用
GCoff内存偿还阶段,将过多的内存归还给操作系统,写屏障敞开 赋值器处于并发状态,GC没有运行sweep在后盾运行,写屏障没有启用
内存逃逸(逃逸剖析):https://segmentfault.com/a/1190000040450335
golang逃逸剖析指令:go build -gcflags '-m'栈对象逃逸到堆上,少逃逸是为了缩小对象在堆上调配,升高堆内存增长率,缩小GC,从而缩小没必要的GC占用CPU利用率。内部援用(指针逃逸)、动静类型逃逸(理解interface{}的内部结构后,能够归并到指针逃逸)、栈空间有余逃逸(对象太大)、闭包援用对象逃逸、slice扩容逃逸,初始化时是调配在栈上的,运行时扩容会逃到堆上(无奈确定栈空间而逃逸)。
golang协程栈空间:用的是间断栈
每个G保护着本人的栈区,它只能本人用不能被其余G用。G运行时栈区会按需增长和膨胀,初始值是2KB(线程2M),不够时是成倍增长,栈空间最大限度在64位零碎上是1GB,32位零碎是250MB 。协程栈空间也是从堆内存里调配的,有全局缓存和本地缓存。 栈增长:间断栈其外围原理是每当程序的栈空间有余要栈增长时,开拓一片更大的栈空间(newstack)并将原栈中的所有值都迁徙copystack到新栈中,旧栈则stackfree回收掉,新的局部变量或者函数调用就有短缺的内存空间,栈增长是成倍扩容的。栈膨胀:运行时栈内存应用有余1/4时会缩容(shrinkstack),会调用开拓新的栈空间并runtime.copystack缩容,大小是原始栈的一半,若新栈大小低于G最低限度2KB,缩容过程就会进行,GC时发动栈膨胀。新栈的初始化和数据复制是一个简略的过程,但这不是整个过程中最简单的中央,还要将指向源栈中的内存指向新栈。所有指针都被调整后,通过runtime.stackfree开释旧栈的内存空间。
golang 协程切换的机会:
1、select阻塞时,2、io(文件,网络)读写阻塞时,3、channel阻塞时,4、加锁/期待锁,5、sleep时,6 、GC时:GC的gcBgMarkWoker会抢占P,7 、零碎调用和抢占时 sysmon线程会监控并把他们调度走,8、runtime.Gosched()。这些阻塞会调用gopark把协程切换到对应的构造体里挂起,当就绪时goready会把他们扔回P的本地队列期待调度。零碎调用工夫过长时会切出去独立线程解决。
golang cpu利用率:死循环或过程有大量计算、协程、GC在执行垃圾回收时会占用所有cpu的25%。负载高:还是零碎调用导致线程数多,工作数过多。
P的最大下限:Go1.10开始,P没有限度,运行时不限度GOMAXPROCS,之前限度为1024当初是int32的最大值,但也受内存限度。切片allp保留全副p。
panic触发场景:数组/切片越界、空指针异样、类型断言不匹配、除0、向已敞开channel发消息、反复敞开channel、敞开未初始化的channel、拜访未初始化map、sync计数正数、过早敞开HTTP响应体
本文由mdnice多平台公布