One neat upgrade in Debian's recent 5.0.0 release1 was Squid 2.7. In this bandwidth-starved corner of the world, a caching proxy is a nice addition to a network, as it should shave at least 10% off your monthly bandwidth usage. However, the recent rise of CDNs has made many objects that should be highly cacheable, un-cacheable.
For example, a YouTube video has a static ID. The same piece of video will always have the same ID, it'll never be replaced by anything else (except a "sorry this is no longer available" notice). But it's served from one of many delivery servers. If I watch it once, it may come from
http://v3.cache.googlevideo.com/videoplayback?id=0123456789abcdef&itag=34&ip=1.2.3.4®ion=0&signature=5B1BA40D8464F2303DDDD59B2586C10A0AEFAD19.169DA15A09AB88E824DE63DF138F0D835295463B&sver=2&expire=1234714137&key=yt1&ipbits=0
But the next time it may come from v15.cache.googlevideo.com
. And that's not all, the signature parameter is unique (to protect against hot-linking) as well as other not-static parameters.
Basically, any proxy will probably refuse to cache it (because of all the parameters) and if it did, it'd be a waste of space because the signature would ensure that no one would ever access that cached item again.
I came across a page on the squid wiki that addresses a solution to this.
Squid 2.7 introduces the concept of a storeurl_rewrite_program
which gets a chance to rewrite any URL before storing / accessing an item in the cache. Thus we could rewrite our example file to
http://cdn.googlevideo.com.SQUIDINTERNAL/videoplayback?id=0123456789abcdef&itag=34
We've normalised the URL and kept the only two parameters that matter, the video id and the itag which specifies the video quality level.
The squid wiki page I mentioned includes a sample perl script to perform this rewrite. They don't include the itag, and my perl isn't good enough to fix that without making a dog's breakfast of it, so I re-wrote it in Python. You can find it at the end of this post. Each line the rewrite program reads contains a concurrency ID, the URL to be rewritten, and some parameters. We output the concurrency ID and the URL to rewrite to.
The concurrency ID is a way to use a single script to process rewrites from different squid threads in parallel. The documentation is this is almost non-existant, but if you specify a non-zero storeurl_rewrite_concurrency
each request and response will be prepended with a numeric ID. The perl script concatenated this directly before the re-written URL, but I separate them with a space. Both seem to work. (Bad documentation sucks)
All that's left is to tell Squid to use this, and to override the caching rules on these URLs.
storeurl_rewrite_program /usr/local/bin/storeurl-youtube.py
storeurl_rewrite_children 1
storeurl_rewrite_concurrency 10
# The keyword for all youtube video files are "get_video?", "videodownload?" and "videoplaybeck?id"
# The "\.(jp(e?g|e|2)|gif|png|tiff?|bmp|ico|flv)\?" is only for pictures and other videos
acl store_rewrite_list urlpath_regex \/(get_video\?|videodownload\?|videoplayback\?id) \.(jp(e?g|e|2)|gif|png|tiff?|bmp|ico|flv)\? \/ads\?
acl store_rewrite_list_web url_regex ^http:\/\/([A-Za-z-]+[0-9]+)*\.[A-Za-z]*\.[A-Za-z]*
acl store_rewrite_list_path urlpath_regex \.(jp(e?g|e|2)|gif|png|tiff?|bmp|ico|flv)$
acl store_rewrite_list_web_CDN url_regex ^http:\/\/[a-z]+[0-9]\.google\.com doubleclick\.net
# Rewrite youtube URLs
storeurl_access allow store_rewrite_list
# this is not related to youtube video its only for CDN pictures
storeurl_access allow store_rewrite_list_web_CDN
storeurl_access allow store_rewrite_list_web store_rewrite_list_path
storeurl_access deny all
# Default refresh_patterns
refresh_pattern ^ftp: 1440 20% 10080
refresh_pattern ^gopher: 1440 0% 1440
refresh_pattern -i (/cgi-bin/|\?) 0 0% 0
# Updates (unrelated to this post, but useful settings to have):
refresh_pattern windowsupdate.com/.*\.(cab|exe)(\?|$) 518400 100% 518400 reload-into-ims
refresh_pattern update.microsoft.com/.*\.(cab|exe)(\?|$) 518400 100% 518400 reload-into-ims
refresh_pattern download.microsoft.com/.*\.(cab|exe)(\?|$) 518400 100% 518400 reload-into-ims
refresh_pattern (Release|Package(.gz)*)$ 0 20% 2880
refresh_pattern \.deb$ 518400 100% 518400 override-expire
# Youtube:
refresh_pattern -i (get_video\?|videodownload\?|videoplayback\?) 161280 50000% 525948 override-expire ignore-reload
# Other long-lived items
refresh_pattern -i \.(jp(e?g|e|2)|gif|png|tiff?|bmp|ico|flv)(\?|$) 161280 3000% 525948 override-expire reload-into-ims
refresh_pattern . 0 20% 4320
# All of the above can cause a redirect loop when the server
# doesn't send a "Cache-control: no-cache" header with a 302 redirect.
# This is a work-around.
minimum_object_size 512 bytes
Done. And it seems to be working relatively well. If only I'd set this up last year when I had pesky house-mates watching youtube all day ;-)
It should of course be noted that doing this instructs your Squid Proxy to break rules.
Both override-expire
and ignore-reload
violate guarantees that the HTTP standards provide the browser and web-server about their communication with each other.
They are relatively benign changes, but illegal nonetheless.
And it goes without saying that rewriting the URLs of stored objects could cause some major breakage by assuming that different objects (with different URLs) are the same.
The provided regexes seem sane enough to not assume that this won't happen, but YMMV.
#!/usr/bin/env python
# vim:et:ts=4:sw=4:
import re
import sys
import urlparse
youtube_getvid_res = [
re.compile(r"^http:\/\/([A-Za-z]*?)-(.*?)\.(.*)\.youtube\.com\/get_video\?video_id=(.*?)&(.*?)$"),
re.compile(r"^http:\/\/(.*?)\/get_video\?video_id=(.*?)&(.*?)$"),
re.compile(r"^http:\/\/(.*?)video_id=(.*?)&(.*?)$"),
]
youtube_playback_re = re.compile(r"^http:\/\/(.*?)\/videoplayback\?id=(.*?)&(.*?)$")
others = [
(re.compile(r"^http:\/\/(.*?)\/(ads)\?(?:.*?)$"), "http://%s/%s"),
(re.compile(r"^http:\/\/(?:.*?)\.yimg\.com\/(?:.*?)\.yimg\.com\/(.*?)\?(?:.*?)$"), "http://cdn.yimg.com/%s"),
(re.compile(r"^http:\/\/(?:(?:[A-Za-z]+[0-9-.]+)*?)\.(.*?)\.(.*?)\/(.*?)\.(.*?)\?(?:.*?)$"), "http://cdn.%s.%s.SQUIDINTERNAL/%s.%s"),
(re.compile(r"^http:\/\/(?:(?:[A-Za-z]+[0-9-.]+)*?)\.(.*?)\.(.*?)\/(.*?)\.(.{3,5})$"), "http://cdn.%s.%s.SQUIDINTERNAL/%s.%s"),
(re.compile(r"^http:\/\/(?:(?:[A-Za-z]+[0-9-.]+)*?)\.(.*?)\.(.*?)\/(.*?)$"), "http://cdn.%s.%s.SQUIDINTERNAL/%s"),
(re.compile(r"^http:\/\/(.*?)\/(.*?)\.(jp(?:e?g|e|2)|gif|png|tiff?|bmp|ico|flv)\?(?:.*?)$"), "http://%s/%s.%s"),
(re.compile(r"^http:\/\/(.*?)\/(.*?)\;(?:.*?)$"), "http://%s/%s"),
]
def parse_params(url):
"Convert a URL's set of GET parameters into a dictionary"
params = {}
for param in urlparse.urlsplit(url)[3].split("&"):
if "=" in param:
n, p = param.split("=", 1)
params[n] = p
return params
while True:
line = sys.stdin.readline()
if line == "":
break
try:
channel, url, other = line.split(" ", 2)
matched = False
for re in youtube_getvid_res:
if re.match(url):
params = parse_params(url)
if "fmt" in params:
print channel, "http://video-srv.youtube.com.SQUIDINTERNAL/get_video?video_id=%s&fmt=%s" % (params["video_id"], params["fmt"])
else:
print channel, "http://video-srv.youtube.com.SQUIDINTERNAL/get_video?video_id=%s" % params["video_id"]
matched = True
break
if not matched and youtube_playback_re.match(url):
params = parse_params(url)
if "itag" in params:
print channel, "http://video-srv.youtube.com.SQUIDINTERNAL/videoplayback?id=%s&itag=%s" % (params["id"], params["itag"])
else:
print channel, "http://video-srv.youtube.com.SQUIDINTERNAL/videoplayback?id=%s" % params["id"]
matched = True
if not matched:
for re, pattern in others:
m = re.match(url)
if m:
print channel, pattern % m.groups()
matched = True
break
if not matched:
print channel, url
except Exception:
# For Debugging only. In production we want this to never die.
#raise
print line
sys.stdout.flush()
Comments
Nice work
The thing that bothers me about youtube, more than that it reloads videos on every visit (although that is stupid), is that it reloads videos every time you push the play button. This has happened to me: "Hey, come look at this cool video I just laboriously buffered then watched!" "Oh, OK, let's just wait 20 minutes for it to buffer again"
Of course, this seems like it solves both problems.
Upgrading quality
It seems this will always ensure you get the quality you asked for. But it seems to me that you don't mind, as long as you get *at least* the quality you ask for. But this can't be done by canonicalizing URLs. Can it?
Interesting thought
No, I don't know how best to handle that, without knowing exactly what's cached or only downloading specific qualities.
yotuube Video keyword
Things have changed this time videoplayback\?id -> videoplayback.*id
youtube video quality
Up to now I don't see the difference between high quality and normal quality(although both still low quality) in the URL. Maybe they call it high quality because its the original file uploaded by the user. By the way HOME is true high quality I've ever seen on youtube.
by the way whats good thing about python vs perl? I'm only concern about speed.
I only know assembly. I'm a beginner with this languages. Thats why I'm toying squid.
minimum_object_size 512 bytes
you'll lost all small content
mem object in hit has mis-matched
i have try your post, and i found these in /var/log/squid/cache.log
================================================
2009/07/01 02:27:02| clientCacheHit: request has store_url 'http://cdn.static.zynga.com.SQUIDINTERNAL/zbar-new/images/trans/yoville-mini-button.png'; mem object in hit has mis-matched url 'http://zbar2.static.zynga.com/zbar-new/images/trans/yoville-mini-button.png?x=10001'!
2009/07/01 02:27:02| clientCacheHit: request has store_url 'http://cdn.static.zynga.com.SQUIDINTERNAL/zbar-new/images/trans/streetracing-mini-button.png'; mem object in hit has mis-matched url 'http://zbar2.static.zynga.com/zbar-new/images/trans/streetracing-mini-button.png?x=10001'!
2009/07/01 02:27:02| clientCacheHit: request has store_url 'http://cdn.static.zynga.com.SQUIDINTERNAL/zbar-new/images/trans/pirates-mini-button.png'; mem object in hit has mis-matched url 'http://zbar2.static.zynga.com/zbar-new/images/trans/pirates-mini-button.png?x=10001'!
2009/07/01 02:27:02| clientCacheHit: request has store_url 'http://cdn.static.zynga.com.SQUIDINTERNAL/zbar-new/images/trans/vampires-mini-button.png'; mem object in hit has mis-matched url 'http://zbar2.static.zynga.com/zbar-new/images/trans/vampires-mini-button.png?x=10001'!
2009/07/01 02:27:02| clientCacheHit: request has store_url 'http://cdn.static.zynga.com.SQUIDINTERNAL/zbar-new/images/trans/fashionwars-mini-button.png'; mem object in hit has mis-matched url 'http://zbar2.static.zynga.com/zbar-new/images/trans/fashionwars-mini-button.png?x=10001'!
2009/07/01 02:27:02| clientCacheHit: request has store_url 'http://cdn.static.zynga.com.SQUIDINTERNAL/zbar-new/templates/facebook_dropdown_allgames/images/bttn_moreGames.png'; mem object in hit has mis-matched url 'http://zbar2.static.zynga.com/zbar-new/templates/facebook_dropdown_allgames/images/bttn_moreGames.png?x=10001'!
2009/07/01 03:28:33| clientCacheHit: request has store_url 'http://cdn.static.zynga.com.SQUIDINTERNAL/zbar-new/images/icons/mafiawars-mini-button.jpg'; mem object in hit has mis-matched url 'http://zbar2.static.zynga.com/zbar-new/images/icons/mafiawars-mini-button.jpg?x=10001'!
2009/07/01 03:28:35| clientCacheHit: request has store_url 'http://cdn.static.zynga.com.SQUIDINTERNAL/zbar-new/images/trans/mafiawars-mini-button.png'; mem object in hit has mis-matched url 'http://zbar2.static.zynga.com/zbar-new/images/trans/mafiawars-mini-button.png?x=10001'!
2009/07/01 04:18:30| storeClientReadHeader: URL mismatch
2009/07/01 04:18:30| {http://static.ak.fbcdn.net/images/icons/fbpage.gif} != {http://static.ak.fbcdn.net/images/icons/fbpage.gif?8:67540}
===============================================
what should i do ??
Seem like a documented but
Seem like a documented but unloved bug.
"storeurl_rewrite mismatched when object stored on memory"
http://bugs.squid-cache.org/show_bug.cgi?id=2248
I am not very good at all
I am not very good at all these codes, so it is a little bit difficult for me to understand it. I will show your article to my friend and together we will be able to do it.
Pingback
Repo proxy cache
Have you come across squirm (http://squirm.foote.com.au/) which was for redirect_program (now url_rewrite_program) but could easily be customised for storeurl_rewrite_program.
It's a shame they haven't yet ported the storeurl_rewrite feature to the 3.x release as this would make it really easy to create a repo cache for CentOS or any other one that uses a mirror list. Yum first fetches a list of repo mirrors then chooses one of them to fetch the packages from. Using the storeurl_rewrute_program directive you could then normalise the store url for use between all of the returned mirrors.
Post new comment