2024年1月21日星期日

udpxy参数设置误区

发现udpxy转发的视频流卡顿/花屏,要不要调整一下参数?
先说一下结论:调整参数没啥用,还很容易适得其反

之前折腾过udpxy的参数,发现几乎没有什么效果,最近由于某个原因看了一下udpxy源码,发现之前存在很多理解上的问题,他这个文档写的非常简略,所以很容易误导,记录一下以备大家继续走弯路。
udpxy的设计是一个单线程处理模式,其核心逻辑是这样工作的,先从源socket读一个或者多个数据包(man文档中叫messages),然后根据不同的协议处理一下,对于RTP协议来说就是去掉packet的header和最后的padding字节,然后把写入HTTP端TCP流,然后再去读后面的数据包,无限循环。这个设计非常简单而且有效,但是也存在一些容易误会的地方。

首先说一下常见的 -B 和 -R 参数,看文档写的是:
-B <sizeK> Buffer size (65536, 32Kb, 1Mb) for inbound (multicast) data [default = 2048 bytes].
-R <msgs> Maximum number of messages to buffer (-1 = all) [default = 1].

看代码发现,这两个参数其实是有关联的,这里的buffer是指接收到多播数据之后处理时的buffer,而一次接受和处理多少个多播消息(message),受控于后面这个参数,如果不改后者,前者设置再大也是没有意义的。而且如果 msgs * 1500 (MTU) 如果大于max buffer size的话,也会直接调大max buffer size,所以本质上只要有这个 -R 参数即可,前面这个选项有点冗余。另外现在很多IPTV流都是MPEG-TS流,这个时候message大小大概在1300多,一次处理一个message和多个message在现在的路由器CPU下性能差距并不大,而一次多读message可能还需要额外的等待,得不偿失。

紧跟着上面参数的还有一个 -H 参数,这个参数是指在数据处理过程中,最多处理时长,默认为1s,超过这个时间就不会继续从多播socket读后续的消息,而是直接转发。这个参数的意义可能仅仅在于如果上面的最大消息数设置过大时,避免处理时间过长导致问题,但对于现在的硬件来说,这个基本上也没啥用。

还有一个参数是 -M ,从文档来说是隔一段时间就重新renew一下多播组的订阅关系,这个参数仅仅在看了一段时间后突然没信号了这种情况才有意义,现在我们大部分IPTV都不会存在这个问题,反而因为重新renew,有可能导致消息的中断和重发,出现问题。

由于UDP是不可靠传输协议,所以RTP有个设计是每个消息包含一个序列号,让接收端可以根据这个序列号重排序,不过udpxy并没有实现重排序,所以如果发生了偶然的需要重排序的情况,udpxy发给下游的数据还是按照接收顺序发送的,这样就可能导致花屏和短暂卡顿,由于udpxy的作者已经明确不维护这个程序了,所以这个问题可能只能等别的项目来解决了。




2022年3月5日星期六

理解IPv6的一个基本机制

刚开始接触IPv6的时候,对于link-local地址(fe00::/8)是不太关注的。

最近在尝试搞一个双栈旁路由,在不对出口光猫做hack的情况下,通过旁路由发送高优先级的RA消息实现了所有IPv6流量通过旁路由转发的能力,做完了之后发现其实挺简单的,一开始就是走了很多弯路。

我的网络配置有点特殊,旁路由有双网卡分别接入了光猫LAN口和交换机,而交换机也接入了LAN口,所以本质上双网卡接入了同一个network segment,在IPv4下,需要非常小心的处理ARP相关的事情,通过source route来解决各种冲突的问题,到了IPv6中突然发现一切都不一样了。

其中最关键的在于IPv4中的ARP在IPv6中已经不存在了,而是换成了完全不一样的基于link-local地址进行邻居发现和通信,由于每个interface都会自动获得link-local的地址,其实也就拥有了基本的IP通信能力,所以一个主机两个网卡都接入同一个network segment的时候,不需要再处理ARP相关的问题,由于link-local地址是一个真正的IP地址,所以所有的数据都是在IP之上传递的,这样就可以通过各种IP层工具来控制数据包了。

由于开始的时候没有意识到这个区别,所以一开始花了很多时间解决如何同时分配IPv6地址给两个网卡,同时还要保证路由的正确性。其实由于我的目的是通过其中一个接口接收LAN请求,分流之后转发,所以其实只需要一个link-local就完全够用了,根本不需要配置多个全局IPv6地址,这样也不需要搞各种路由规则了。


2021年11月19日星期五

联通HN8346X6光猫简单折腾

联通送的HN8346X6还不错,自带WIFI6据网上评测还可以,但是开桥接,端口映射等稍微复杂一点的功能联通版界面就一概欠奉了,为了找回这些光猫原本就有的功能,尝试折腾了一下。

首先想的是能不能找到管理员密码,根据网上的方法,首先得补全shell,一通搜索之后,终于找到一个人提供了一个改shell的方法:
https://www.right.com.cn/forum/thread-4060870-1-1.html

断开光猫光纤,有线连上任意端口,通过最新版的ONT工具,选择升级选项,加载补全shell的文件(应该只是一个配置文件,所以非常小),等待成功即可。
之后就可以telnet,账号root,密码adminHW,登录成功之后su,通过华为密码计算器算出challenge密码,成功之后输入shell即可进入busybox的shell。 

虽然拿到了root权限,也找到了hw_ctree.xml并且解密成功,但是发现这个文件里根本就没有管理员配置项,尝试自己添加了一个,但是在光猫登录页就是不让登陆,看了一下其实是JavaScript代码做的检查,但是本人不是前端工程师,懒得折腾这个前端代码了,还是直接刷华为模式吧。 所以就执行光猫自带的restorehwmode.sh,恢复了华为界面。

特别提醒恢复华为模式貌似无法再回到联通界面,所以刷之前一定需要记住INTERNET,IPTV,组播VLAN之类的配置信息,要不然就只能网上找同一地区的信息了。

恢复华为界面之后,用华为默认账号telecomadmin密码admintelecom即可登陆网页界面,剩下的就没啥好说的了。

相关工具分享:https://115.com/s/swnenir3zb5 访问码:b2d1

2015年4月1日星期三

Hidden command options of ExpressCache (OEM version)

My Lenovo ThinkPad comes with a 16GB SSD installed as cache, then I install another 128G M.2 SSD as primary OS drive, after re-installing Windows, the cache software is gone, so that the 16GB SSD becomes useless.

Fortunately, the cache software is available for download from Lenovo's website. So that I downloaded it and installed it, while I realize this cache should be used for the hard drive only, not for the SSD. While looks like the eccmd provided only very limited options:


ExpressCache Command Version 1.3.110.0
Copyright?2010-2013 Condusiv Technologies.
Date Time: 1/21/2015 20:27:16:729 (THINKPAD #2)

USAGE:

 ECCmd [-NOLOGO]
       [-INFO | -PARTITION | -FORMAT]

 -NOLOGO              - No copyright message will be displayed.
 -INFO                - Display ExpressCache activity information.
 -PARTITION           - Create an ExpressCache partition.
         [DriveId]    - Optional drive ID
         [PartSize]   - Optional partition size in MB
 -FORMAT              - Format the cache volume.


After searching Google, I found there are some options in previous version:

 -EXCLUDE             - Exclude a drive from being cached.
         DriveLetter  - Drive letter
 -CLEAREXCLUSIONS     - Clear all cache exclusions.
 -PRELOAD             - Preload a folder or a file into the cache
         FileName     - File or folder name to be cached
         [UsageCount] - Optional file usage count.

This is just what I need, keeping here since there are very few pages mentioned those hidden options.

2015年2月9日星期一

偶然读到LMAX Disruptor

因为一个偶然的机会读到LMAX Disruptor,这个项目里最核心的那个Ring Buffer真是一个巧妙的解决方案。 当年在英特尔的时候也需要解决类似的问题,可惜还没有到需要解决这么极致的性能问题的时候项目就夭折了。

2015年1月6日星期二

sun.nio.ch.Util managed direct buffer and thread reusing

Background

As I mentioned in previous blog, there is a memory fragmentation problem within our application, and tunning the JVM parameters can resolve the problem, but it doesn't answer the question who is creating so many direct buffers and use them for short time? Because it's against the recommendation of ByteBuffer.allocateDirect():
It is therefore recommended that direct buffers be allocated primarily for large, long-lived buffers that are subject to the underlying system's native I/O operations.

Analysis

To answer this question, I created a simple program to monitor the caller of ByteBuffer.allocateDirect(int) through JDI, and got following output (some stack are omitted):

java.nio.ByteBuffer.allocateDirect(int): count=124, size=13463718
  sun.nio.ch.Util.getTemporaryDirectBuffer(int): count=124, size=13463718
    sun.nio.ch.IOUtil.write(java.io.FileDescriptor, java.nio.ByteBuffer, long, sun.nio.ch.NativeDispatcher): count=1, size=447850
      sun.nio.ch.SocketChannelImpl.write(java.nio.ByteBuffer): count=1, size=447850
        org.eclipse.jetty.io.ChannelEndPoint.flush(java.nio.ByteBuffer[]): count=1, size=447850
    sun.nio.ch.IOUtil.write(java.io.FileDescriptor, java.nio.ByteBuffer[], int, int, sun.nio.ch.NativeDispatcher): count=102, size=12819260
      sun.nio.ch.SocketChannelImpl.write(java.nio.ByteBuffer[], int, int): count=102, size=12819260
        org.eclipse.jetty.io.ChannelEndPoint.flush(java.nio.ByteBuffer[]): count=102, size=12819260
    sun.nio.ch.IOUtil.read(java.io.FileDescriptor, java.nio.ByteBuffer, long, sun.nio.ch.NativeDispatcher): count=21, size=196608
      sun.nio.ch.SocketChannelImpl.read(java.nio.ByteBuffer): count=21, size=196608
        org.eclipse.jetty.io.ChannelEndPoint.fill(java.nio.ByteBuffer): count=21, size=196608
The count is the number of direct buffers, and the size is the total size of buffers. According the monitoring result, those direct buffers are all allocated by JDK default implementation of SocketChannel, when the user pass a non-direct buffer to perform I/O. After read the source code of sun.nio.ch.Util, I found it uses a thread local cache to keep the direct buffer. While the sun.nio.ch.Util is designed carefully to limit the total number of cached buffers (8 per thread), and clean direct buffer while it's removed from the cache. The only possible reason is there are too many new threads, and the buffers in cache are not used at all.

Root Cause

Our application uses Jetty to handle HTTP requests, and there is a custom class wraps Jetty Server, in this wrapper class, a ThreadPoolExecutor is used like following:

ExecutorService executor = new ThreadPoolExecutor(
 minThreads,
 maxThreads,
 maxIdleTimeMs,
 TimeUnit.MILLISECONDS,
 new SynchronousQueue());
Server server = new Server(new ExecutorThreadPool(executor));
Unfortunately, the minThreads is 2 and maxIdleTimeMs is 5000ms, and the Jetty Server will use 12 threads from the pool for accepting and selector. Which means the total number of threads is always bigger than the minThreads. When the service is not very busy, a worker thread will be discarded, and a new worker thread will be created when a new request comes. In this situation, the cached buffers in sun.nio.ch.Util will no longer be used and will only been collected when GC triggered.

2015年1月2日星期五

Understanding about CMSInitiatingOccupancyFraction and UseCMSInitiatingOccupancyOnly

While reading the Useful JVM Flags – Part 7 (CMS Collector), I was impressed that CMSInitiatingOccupancyFraction was useless when UseCMSInitiatingOccupancyOnly is false (default) except the first CMS collection:
We can use the flag -XX+UseCMSInitiatingOccupancyOnly to instruct the JVM not to base its decision when to start a CMS cycle on run time statistics. Instead, when this flag is enabled, the JVM uses the value of CMSInitiatingOccupancyFraction for every CMS cycle, not just for the first one.
After checking the source code, I found this statement is inaccurate, a more accurate statement would be:
When UseCMSInitiatingOccupancyOnly is false (default), a CMS collection may be triggered even the actual occupancy is smaller than the specified CMSInitiatingOccupancyFraction value. In other words, when actual occupancy is greater than the specified CMSInitiatingOccupancyFraction value, a CMS collection will be triggered.

Detail Explanation

Code snippet from OpenJDK (openjdk/hotspot/src/share/vm/gc_implementation/concurrentMarkSweep/concurrentMarkSweepGeneration.cpp):

  // If the estimated time to complete a cms collection (cms_duration())
  // is less than the estimated time remaining until the cms generation
  // is full, start a collection.
  if (!UseCMSInitiatingOccupancyOnly) {
    if (stats().valid()) {
      if (stats().time_until_cms_start() == 0.0) {
        return true;
      }
    } else {
      // We want to conservatively collect somewhat early in order
      // to try and "bootstrap" our CMS/promotion statistics;
      // this branch will not fire after the first successful CMS
      // collection because the stats should then be valid.
      if (_cmsGen->occupancy() >= _bootstrap_occupancy) {
        if (Verbose && PrintGCDetails) {
          gclog_or_tty->print_cr(
            " CMSCollector: collect for bootstrapping statistics:"
            " occupancy = %f, boot occupancy = %f", _cmsGen->occupancy(),
            _bootstrap_occupancy);
        }
        return true;
      }
    }
  }

  // Otherwise, we start a collection cycle if either the perm gen or
  // old gen want a collection cycle started. Each may use
  // an appropriate criterion for making this decision.
  // XXX We need to make sure that the gen expansion
  // criterion dovetails well with this. XXX NEED TO FIX THIS
  if (_cmsGen->should_concurrent_collect()) {
    if (Verbose && PrintGCDetails) {
      gclog_or_tty->print_cr("CMS old gen initiated");
    }
    return true;
  }
In above code, the _cmsGen->should_concurrent_collect() is always been called, unless it's already determined that a collection is needed. In the implementation of _cmsGen->should_concurrent_collect(), the CMSInitiatingOccupancyFraction value is checked at beginning.

bool ConcurrentMarkSweepGeneration::should_concurrent_collect() const {

  assert_lock_strong(freelistLock());
  if (occupancy() > initiating_occupancy()) {
    if (PrintGCDetails && Verbose) {
      gclog_or_tty->print(" %s: collect because of occupancy %f / %f  ",
        short_name(), occupancy(), initiating_occupancy());
    }
    return true;
  }
  if (UseCMSInitiatingOccupancyOnly) {
    return false;
  }
  if (expansion_cause() == CMSExpansionCause::_satisfy_allocation) {
    if (PrintGCDetails && Verbose) {
      gclog_or_tty->print(" %s: collect because expanded for allocation ",
        short_name());
    }
    return true;
  }
  if (_cmsSpace->should_concurrent_collect()) {
    if (PrintGCDetails && Verbose) {
      gclog_or_tty->print(" %s: collect because cmsSpace says so ",
        short_name());
    }
    return true;
  }
  return false;
}
From the above code, it's easy to find out that CMSBootstrapOccupancy is been used for first collection if UseCMSInitiatingOccupancyOnly is false.

Summary

The UseCMSInitiatingOccupancyOnly need to be set to true only if you want to avoid the early collection before occupancy reaches the specified value. Looks it's not the case when CMSInitiatingOccupancyFraction is set to a small value. For example you application allocated direct buffers frequently and you may want to collect garbage even the old generation utilization is quite low.